This is part 10 of the series of post that I am doing about building a Continuous Integration Environment using Sitecore, TeamCity, Octopus Deploy, and Unicorn.
Part 1 – Setting Up TeamCity
Part 2 – Setting up OctopusDeploy
Part 3 – Setting up SQL Server for Sitecore
Part 4 – IIS
Part 5 – Octopus Environment and Deployment Configuration
Part 6 – TeamCity Project
Part 7 – OctopusDeploy Project
Part 8 – Sitecore Item Synchronization using Unicorn
Part 9 – Displaying Build number on Sitecore Home Page & Tagging Git
Part 10 – This post
Part 11 – Deploying to a Sitecore Multi-Instance Configuration
I was asked how I had set up my config transformations for variable substitutions for Sitecore patch files. I thought it would be better to describe this in a separate post rather than just inside a comment.
I am not doing anything special. I am just using standard transforms.
As per Sitecore best practice, every alteration I have made to the solution, that is Sitecore specific, I have attempted to place into a separate config file, which for the sake of simplicity I have named z_project.config and has been placed in my App_Config include folder.
However there are a few instances where I still need to transform items within appSettings.config, connectionStrings.config, and web.config files.
Everything that I have done is based on this Octopus Deploy – Configuration Files
When you want to transform the z_project.config file per environment, you need to supply a separate transformation config file. Eg. for my CI environment, I need to have a file called z_project.CI.config, ditto for my environment named QA, I will need to have a file z_project.QA.config.
As previously mentioned I need to make some transformations to my connectionString.config file and a few minor changes to my web.config file, so for these I have created connectionString.CI.config, and web.CI.config.
I do have a separate appSettings.config file, but I have found that I do not need to have a separate transformation files for this, and have been able to just set up variables within Octopus, and have these values used during the deployment process.
appSettings.config
I generally always extract my <appSettings> node out of the web.config file, into its own file, and then use the configSource attribute to use the new file i.e. <appSettings configSource=”App_Config\AppSettings.config” />
<?xml version="1.0"?> <appSettings> <add key="debugEmail" value="true"/> <add key="debugEmailRecipient" value="darren.guy@xxxx.co.uk"/> </appSettings>
This is a small snippet from my appSettings.config file. The code has been developed using a feature switch so that when it is on a non development environment, then any email sent out should not be forwarded to its original recipient, but is instead forwarded to another account.
Generally I should have no need to change any of these values, except with I deploy to production. But within Octopus I have created the following variables
Name | Value | Scope* |
debugEmail | true | CI,QA |
debugEmail | false | PRD |
debugEmailRecipient | darren.guy@xxx.co.uk | CI |
debugEmailRecipient | qa.team@xxx.co.uk | QA |
debugEmailRecipient | PRD |
* For each scope, I was using Environments.
For the variable debugEmail, this was the Variable scope defined:
All the variables had Environment variable scope defined similarly.
Within my step template “Deploy Site” that I had created:
- I had ticked the option Configuration Variables Replace appSettings and connectionString entities in any .config file.
- I also checked XML transforms Automatically run configuration transformation files
- Finally, I had specified Target files to substitute variables in files:
App_Config\Include\Z_#{Octopus.Project.Name}.#{Octopus.Environment.Name}.config
App_Config\Include\SiteDefinition.#{Octopus.Environment.Name}.config
Web.#{Octopus.Environment.Name}.config
When Octopus is deploying to that environment it will take the variable value, based on the environment scope, and correctly transform the appSettings.config file
Therefore when i deploy to CI, my appSettings.config file will be
<?xml version="1.0"?> <appSettings> <add key="debugEmail" value="true"/> <add key="debugEmailRecipient" value="darren.guy@xxxx.co.uk"/> </appSettings>
When deployed to QA, the file will be transformed to
<?xml version="1.0"?> <appSettings> <add key="debugEmail" value="true"/> <add key="debugEmailRecipient" value="qa.team@xxxx.co.uk"/> </appSettings>
And finally when the file is deployed to production.
<?xml version="1.0"?> <appSettings> <add key="debugEmail" value="false"/> <add key="debugEmailRecipient" value=""/> </appSettings>
connectionStrings.config
With my connectionStrings.config file I have done something a little bit different
This is the connectionStrings.config file
<?xml version="1.0" encoding="utf-8"?> <connectionStrings> <add name="core" connectionString="server=(local)\SQL2012;database=Sitecore_Core;Trusted_Connection=True" /> <add name="master" connectionString="server=(local)\SQL2012;database=Sitecore_Master;Trusted_Connection=True" /> <add name="web" connectionString="server=(local)\SQL2012;database=Sitecore_Web;Trusted_Connection=True" /> <add name="analytics" connectionString="server=(local)\SQL2012;database=Sitecore_Analytics;Trusted_Connection=True" /> </connectionStrings>
Instead of using variables within Octopus, I have used a transformation file for each environment. My connectionString.CI.config file is
<?xml version="1.0"?> <connectionStrings xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <add name="core" xdt:Locator="Match(name)" xdt:Transform="Replace" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Core" /> <add name="master" xdt:Locator="Match(name)" xdt:Transform="Replace" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitcore_CI_Master" /> <add name="web" xdt:Locator="Match(name)" xdt:Transform="Replace" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Web" /> <add name="analytics" xdt:Locator="Match(name)" xdt:Transform="Replace" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Analytics" /> </connectionStrings>
When Octopus Deploy applys the transformation, this is the resulting connectionString.config file
<?xml version="1.0" encoding="utf-8"?> <connectionStrings> <add name="core" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Core"/> <add name="master" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Master"/> <add name="web" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Web"/> <add name="analytics" connectionString="server=192.17.200.32\SQL2012;user id=sitecore;password=sitecore;database=Sitecore_CI_Analytics"/> </connectionStrings>
I could have stored these values as variables within Octopus, similar to what I have done with the appSettings.config file but this method works for me
web.config
Changes that I needed to make to my web.config file, I treated similarly to connectingStrings.config, in that I had a separate transformation file, web.CI.config
This is my web.CI.config file
<?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.webServer> <httpProtocol xdt:Transform="Insert"> <customHeaders> <remove name="X-Powered-By" /> <remove name="X-AspNet-Version" /> <remove name="X-AspNetMvc-Version" /> </customHeaders> </httpProtocol> </system.webServer> <system.net> <mailSettings> <smtp deliveryMethod="Network" from="#{email.from}"> <network xdt:Transform="SetAttributes" host="#{smtp.servername}" port="#{smtp.port}" userName="#{smtp.username}" password="#{smtp.password}" /> </smtp> </mailSettings> </system.net> </configuration>
This file is doing two things. It is adding in the nodes to tell IIS not to be too chatty. This is nothing special this should be done as part of following the Security Hardening guide within Sitecore.
However, where I am transforming the SMTP credentials, I am taking the values from variables that have been defined within Octopus Deploy. These variables are defined similarly to how I defined the debugEmail variable for transforming the appSettings.config file. I found that I had to transform the web.config file this way to use variables defined within Octopus Deploy.
Currently with Octopus deploys my solution solution, it will replace all variables that are defined within any .config file before it actions the transformation,
z_project.config
My configuration file changes are similar to what I have set up for web.config
This is a subset of my z_project.config file
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" > <sitecore> <sc.variable name="dataFolder"> <patch:attribute name="value">/Data</patch:attribute> </sc.variable> <settings> <setting name="Rainbow.SFS.SerializationFolderPathMaxLength"> <patch:attribute name="value">110</patch:attribute> </setting> <setting name="InvalidItemNameChars"> <patch:attribute name="value">\/:?"<>|[]-</patch:attribute> </setting> <setting name="ItemNotFoundUrl"> <patch:attribute name="value">/404</patch:attribute> </setting> <setting name="LinkItemNotFoundUrl"> <patch:attribute name="value">/404</patch:attribute> </setting> <setting name="Query.MaxItems"> <patch:attribute name="value">2000</patch:attribute> </setting> </settings> </sitecore> </configuration>
As you can see, i use the project specific config file to change the location of the data directory
This I want to transform when I deploy to different environments the data folder is not in the web root
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <sitecore> <sc.variable name="dataFolder" xdt:Transform="Replace" xdt:Location="Match(name)" set:value="#{data.folder}" /> <settings> <setting name="Login.DisableAutoComplete" xdt:Transform="Insert"> <patch:attribute name="value">true</patch:attribute> </setting> <setting name="Login.DisableRememberMe" xdt:Transform="Insert"> <patch:attribute name="value">true</patch:attribute> </setting> <setting name="MailServer" xdt:Transform="Insert"> <patch:attribute name="value">#{smtp.servername}</patch:attribute> </setting> <setting name="MailServerUserName" xdt:Transform="Insert"> <patch:attribute name="value">#{smtp.username}</patch:attribute> </setting> <setting name="MailServerPassword" xdt:Transform="Insert"> <patch:attribute name="value">#{smtp.password}</patch:attribute> </setting> <setting name="MailServerPort" xdt:Transform="Insert"> <patch:attribute name="value">#{smtp.port}</patch:attribute> </setting> </settings> </sitecore> </configuration>
As detailed above, I have a variable defined within OctopusDeploy, named data.folder, and when Octopus deploys my solution to the CI environment, it will replace the variable in the z_project.CI.config file, before it applies that transformation to z_project.config
Once the deployment is completed, this is what my file contains
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" > <sitecore> <sc.variable name="dataFolder" set:value="D:\Websites\Project.CI\data"/> <settings> <setting name="Rainbow.SFS.SerializationFolderPathMaxLength"> <patch:attribute name="value">110</patch:attribute> </setting> <setting name="InvalidItemNameChars"> <patch:attribute name="value">\/:?"<>|[]-</patch:attribute> </setting> <setting name="ItemNotFoundUrl"> <patch:attribute name="value">/404</patch:attribute> </setting> <setting name="LinkItemNotFoundUrl"> <patch:attribute name="value">/404</patch:attribute> </setting> <setting name="Query.MaxItems"> <patch:attribute name="value">2000</patch:attribute> </setting> <setting name="Login.DisableAutoComplete"> <patch:attribute name="value">true</patch:attribute> </setting> <setting name="Login.DisableRememberMe"> <patch:attribute name="value">true</patch:attribute> </setting> <setting name="MailServer"> <patch:attribute name="value">10.10.11.15</patch:attribute> </setting> <setting name="MailServerUserName"> <patch:attribute name="value"> </patch:attribute> </setting> <setting name="MailServerPassword"> <patch:attribute name="value"> </patch:attribute> </setting> <setting name="MailServerPort"> <patch:attribute name="value">25</patch:attribute> </setting> </settings> </sitecore> </configuration>
The above file is highly abbreviated and does not show my full file.
I actually have three variables defined that I use to deploy my site
Name | Value | Scope* |
base.folder | D:\Websites\#{website.name} | |
data.folder | #{base.folder}\data | |
website.folder | #{base.folder}\Website | |
website.name | #{Octopus.Project.Name}.#{Octopus.Environment.Name} |
* There is no scope configured for any of these variables.
Octopus will ensure that all variables, no matter how nested the variable names are, are all correctly resolved.
*.ENV.config files
The great thing about Octopus Deploy is that it is deletes all the environment specific transformation files after it has completed the deployment, so I never have to worry about rogue files on the file system. However when you are developing locally, Sitecore cannot differentiate between a .config file that it needs to incorporate, and a transformation config file.
The good news is that all you need to do is hide you environment specific config files and Sitecore will not process them. In my solution, I have the following post build step
if $(ConfigurationName) == Debug ( attrib +h $(ProjectDir)App_Config\Include\*.CI.config /s attrib +h $(ProjectDir)App_Config\Include\*.QA.config /s attrib +h $(ProjectDir)App_Config\Include\*.PRD.config /s attrib +h $(ProjectDir)App_Config\Include\*.Debug.config /s attrib +h $(ProjectDir)App_Config\Include\*.Release.config /s )
This makes sure that all the environment specific files are hidden. I develop locally using the Debug configuration. Change this value as required for your environment
Final Thoughts
What I have detailed works for me on this particular solution that I used to generate these posts. You may want to do things slightly differently from what I have done on your own solution
Hi Darren,
Thanks again for these helpful posts.
Just reading towards the end of this one. You mention that Octopus automatically deletes any Environment specific transforms post deployment. I might have misunderstood this to be an OOB feature in Octopus? Or is it actually a step I need to manually add? have you already done that step and I missed it?
Really appreciate your help.
Thanks.
Hmm. I may be mistaken in my comment. When I build my nuspec file, you can include PostDeploy.ps1.
In this file I have this
get-childitem .\ -include *Debug.config, *Release.config, *CI.config, *QA.config, *Production.config, *UAT.config -recurse | foreach ($_) {remove-item $_.fullname -force -Verbose}
and it may be this that is deleting all environment specific config files. I will need to do some testing, and if required, will update the article
Hi Darren,
I actually just added an extra step that recursively looks for transforms (by regex filtering on evironment names) and deletes anything it finds, seems to do the job and similar to what you explained. Thanks a lot for looking into this.