Extending EPiServer MSBuild config transforms to provide server specific configuration for TCP EventReplication and Licenses

I encountered an issue this week setting up the production environment for my current client, as it turned out that we would not be able to use EPiServer‘s UDP multicasting between our servers. Hence, we would have to configure the use of TCP for the EventReplication service; meaning different configuration files for each server. As we always strive to make our deploys as painless as possible (most often meaning automating everything using scripts) there was also an excellent opportunity to improve the distribution of the proper License.config files to each webserver (Nope, no IP-ranges for the license files here). I wrote a new transforms XML file as a supplement to EPiServer’s MSBuild configuration transformations to solve this issue. Everything can be found at GitHub as usual.

Server specific MSBuild config transforms for EPiServer

If you have not familiarized yourself with MSBuild config transforms before, the way to work with them may take a few minutes to grasp. I’ve tried to break this down as much as possible to make things a bit easier. The first thing that I needed to do was to come up with a way to only run my server specific transformation for certain build solution configurations; i.e. the System test (SysTest), Acceptance test (AccTest) and Production environments. Solution configurations such as for instance Local, that had no need for a multi server setup, would need to be omitted.

MSBuild Server specific config transforms - Solution configuration

Boolean properties for MSBuild targets

The first MSBuild property, Configuration, was shamelessly nicked from EPiServer’s own transform files. The second however is the one i created for the bool value HasServerSpecificTransforms.

MyProject.Web\[MSBuild]\ServerSpecificConfigTransforms.xml

\ServerSpecificConfigTransforms.xml" firstline="6"]
<PropertyGroup>
  <Configuration Condition="'$(Configuration)' == ''">Local</Configuration>

  <HasServerSpecificTransforms>false</HasServerSpecificTransforms>
  <HasServerSpecificTransforms Condition="'$(Configuration)' == 'Production' OR '$(Configuration)' == 'AccTest' OR '$(Configuration)' == 'SysTest'">true</HasServerSpecificTransforms>
</PropertyGroup>

An easy way to create boolean properties for an MSBuild target is to first specify the property as false in a PropertyGroup (9), and then overwrite it just below by evaluating a condition (10). In my case I wanted the bool property to return true if the current solution configuration was any of the three previously mentioned ones.

The way to use this bool property value is then very straight forward. In the code snippet below the message text will only be sent to the output console if the current solution configuration has server specific transforms.

<Message Condition="$(HasServerSpecificTransforms)"
         Text="Generating configuration files for $(Configuration) server specific configurations" />

Listing directory names with [System.IO.Directory] in MSBuild transformation

The way that the folder structure is set up in the Visual Studio Solution Explorer is shown in the image below. It is based on EPiServer’s structure, and follows the pattern [Configuration]\{BuildConfiguration}\{ServerName}\{filename}.{ServerName}.config. In the image I have removed most of the configuration files from the webfront directories in order to keep the noise down.

The solution explorer showing the directory structure for MSBuild server specific build transformations.

So in order to get hold of all the proper server names for the currently transforming solution configuration, we add an ItemGroup to the XML file.

<ItemGroup>
  <ServerNames Include="$([System.IO.Directory]::GetDirectories('..\[Configuration]\$(Configuration)'))" />

As the current directory is the one containing the MSBuild transform XML file (i.e. [MSBuild]) the path is relative from there. This will result in the following content in the property ServerNames using the solution configuration Production.

..\[Configuration]\Production\EDITORSERVER01
..\[Configuration]\Production\WEBFRONT01
..\[Configuration]\Production\WEBFRONT02
..\[Configuration]\Production\WEBFRONT03

Simple MSBuild transform target foreach loop for all items in ItemGroup list property

The same transformations should be performed for each server in the array, so a foreach loop would be perfect. The easiest way that I’ve found of doing this is illustrated below.

<Target Name="Build">
  <!-- Lines removed to reduce noise -->

  <!-- Call DoTransforms foreach server in ServerNames -->
  <MSBuild Condition="$(HasServerSpecificTransforms)"
           Projects="$(MSBuildProjectFile)"
           Properties="CurrentServerName=%(ServerNames.Identity)"
           Targets="DoTransforms">
  </MSBuild>

  <!-- Lines removed to reduce noise -->
</Target>

The code above will call a target called DoTransforms (code below) for every item in the property ServerNames (and insert the value in the input property CurrentServerName) if the condition HasServerSpecificTransforms returns true.

Property string value manipulation in MSBuild target using regex

The loop described above will call this target for each item in the ServerNames array, passing the CurrentServerName along as a property. The problem is, this property contains not only the servername, but also the relative path to the directory. In order to get rid of this we employ the use of regular expressions.

<Target Name="DoTransforms">
<PropertyGroup>
<CurrentServerName> $([System.Text.RegularExpressions.Regex]::Replace( $(CurrentServerName), '\.\.\\\[Configuration\]\\$(Configuration)\\', '', System.Text.RegularExpressions.RegexOptions.IgnoreCase))</CurrentServerName>
</PropertyGroup>

Placing a node with the same name as the input property in a PropertyGroup within the target will cause the property to be updated with the return value from the contained expression; in this case, the string being returned after the [System.Text.RegularExpressions.Regex]::Replace method has been called. In the snippet above, the case is ignored even though it is likely not necessary.

Creating server specific transformations with MSBuild

The transformations themselves are also copied from EPiServer’s files, with a few modifications. As EPiServer takes the core transformation files contained in the [Configuration]\EPiServer directory, mashing them up with what’s in [Configuration]\Common, followed with what’s in [Configuration]\{BuildConfiguration}, and puts the resulting file in the web root, this is what will work as source for our own transformations.

<!-- Transform NLog.config -->
<TransformXml
   Condition="Exists('$(ProjectDir)..\[Configuration]\$(Configuration)\$(CurrentServerName)\NLog.$(CurrentServerName).config')"
   Source="$(ProjectDir)..\NLog.config"
   Transform="$(ProjectDir)..\[Configuration]\$(Configuration)\$(CurrentServerName)\NLog.$(CurrentServerName).config"
   Destination="$(ProjectDir)..\NLog.$(CurrentServerName).config" />

The output of this will be a file called for instance NLog.WEBFRONT02.config, placed in the web root directory; you may want to put it somewhere else depending on how you proceed.

The license files that need not be transformed are handled in a similar manner; the proper license file is picked form the current server diretory and placed in the web root under the name License.WEBFRONT02.config, if following the previous example.

<!-- Copy EPiServer License.config -->
<Copy
   Condition="Exists('$(ProjectDir)..\[Configuration]\$(Configuration)\$(CurrentServerName)\License.$(CurrentServerName).config')"
   SourceFiles="$(ProjectDir)..\[Configuration]\$(Configuration)\$(CurrentServerName)\License.$(CurrentServerName).config"
   DestinationFiles="$(ProjectDir)..\License.$(CurrentServerName) .config" />

Removing files using MSBuild transformations

Wrapping this up, we want to delete EPiServer’s vanilla transformations as we will no longer need them. The only thing we are interested in isfor instance our four different versions of the NLog.config files; NLog.EDITORSERVER01.config, NLog.WEBFRONT01.config, NLog.WEBFRONT02.config and NLog.WEBFRONT03.config.

For this, we also use a bit of EPiServer’s old setup, wrapped up in a separate target with a condition wrapped around it.

<CallTarget Condition="$(HasServerSpecificTransforms)"
            Targets="DeleteTemporaryFiles" />

<!-- Removed lines -->

<Target Name="DeleteTemporaryFiles">
  <Delete Files="@(TemporaryFiles)">
     <Output TaskParameter="DeletedFiles"
             PropertyName="deleted" />
  </Delete>

In the transform at GitHub each of the EPiServer transformed files are listed separatly rather than removing *.config from the web root. This is due to us having other things there not supposed to be deleted, and it’s cleaner just to hard code the stuff.

Automatically deploying the correct configuration to each server

The deployment process itself is rather straight forward, it involves a Windows PowerShell script that (except doing the whole deploy, database schema Tarantino updates, backups and so on) looks at the name of the server it’s being run on, and then copies the configuration collection with the corresponding name to the proper location. An example on how the copy parts of such a script may be written can be found in this article: PowerShell script example for copying correct configs based on server machine name in multi server environment.

Since we’re using ImageVault for our EPiServer website at my current client, we also include the ImageVault Core and ImageVault UI configuration files in the transformations; for instance Core’s ImageVault.Core.Host.exe.config, logging and tracing files and so on. The deployment scritpt then replaces these files in the proper ImageVault application instance directory, and restarts the ImageVault Core Service. In this manner we get a good overview of what’s actually deployed on each server and have a convenient way of managing the load balancing configuration for ImageVault.

Leave a Reply