Sitecore in Kubernetes: Using environment variables in web.config instead of environment specific xdt transforms in AKS

Working with Sitecore we usually have a multi environment infrastructure. For example, DEV, Acceptance and Production environments. Potentially, each of them require some environment specific values in web.config. In one of my projects, I had to extend Content-Security-Policy setting with additional URL:

<location path="sitecore">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Content-Security-Policy"               value="default-src 'self' 'unsafe-inline' 'unsafe-eval' https://rendering-dev.sitename.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' 'unsafe-inline' https://fonts.gstatic.com; upgrade-insecure-requests; block-all-mixed-content;"/>
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>

 

Where https://rendering-dev.sitename.com is an environment specific hostname.

First of all I decided to use web XDT transforms like web.config.DEV.xdt, web.config.ACC.xdt and web.config.PROD.xdt and apply them on docker-compose build time. Where DEV, ACC and PROD is a value of an environment variable passed to the Dockerfile. And it works pretty fine when we build our solution locally. But when I started to prepare Build Pipelines in Azure DevOps, I noticed that I don't have an option to specify variables per environment. Because a Build Pipeline doesn’t have Stages and we are not able to link different variable libraries to corresponding stages. Once transformations are applied during a build time, not runtime, I can’t move this operation to Release Pipelines.

I found two ways to solve the issue I faced:

  • Create a separate Build Pipelines for each environment (Stage) which is definitely an option. But I don’t like it because in this case I need to support three almost the same pipelines instead of one
  • Use an environment variable in web.config instead of patching exact value.
By default, we can’t use environment variables in web.config, but since .NET Framework 4.7.1 we can utilize configuration builders which provide a modern and agile mechanism for ASP.NET apps to get configuration values from external sources. One of predefined config builders is used to read values from system environment variables. More info about config builders here.
 
To align it with docker (k8s) build approach we should do the following:

- We need to add a new configBuilder to our web.config. The easiest way to do that is to utilize XDT transforms in one of our projects:

<configBuilders>
    <builders>
      <add name="environment" mode="Expand" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment"
          xdt:Transform="InsertIfMissing" xdt:Locator="Match(name)" />
    </builders>
</configBuilders>

 

Code above adds (if missing) a basic key/value Configuration Builder, called “environment”,  for the .Net Framework that draws from environment variables.

- Update desired web.config property with currently introduced builder and variables. For example:

<location path="sitecore">
    <system.webServer>
      <httpProtocol>
        <customHeaders configBuilders="environment" xdt:Transform="SetAttributes(configBuilders)">
          <add name="Content-Security-Policy" xdt:Transform="Replace" xdt:Locator="Match(name)"
              value="default-src 'self' 'unsafe-inline' 'unsafe-eval' ${RENDERING_HOST_PUBLIC_URI}; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' 'unsafe-inline' https://fonts.gstatic.com; upgrade-insecure-requests; block-all-mixed-content;"/>
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>

 

In the code above we have set the environment config builder for the whole customHeaders. Which means the variables that this section has, will be read from:

  • The values in the web.config file if the keys are not set in environment variables.
  • The values of the environment variable, if set.

Finally, my web.config.xdt looks like below:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <configBuilders>
    <builders>
      <add name="environment" mode="Expand" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment"
          xdt:Transform="InsertIfMissing" xdt:Locator="Match(name)" />
    </builders>
  </configBuilders>
  <location path="sitecore">
    <system.webServer>
      <httpProtocol>
        <customHeaders configBuilders="environment" xdt:Transform="SetAttributes(configBuilders)">
          <add name="Content-Security-Policy" xdt:Transform="Replace" xdt:Locator="Match(name)"
              value="default-src 'self' 'unsafe-inline' 'unsafe-eval' ${RENDERING_HOST_PUBLIC_URI}; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' 'unsafe-inline' https://fonts.gstatic.com; upgrade-insecure-requests; block-all-mixed-content;"/>
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
</configuration>

 

The last thing left is to apply the transform file to the actual web.config placed in a docker image. The code below needs to be added to the Docker file of CM and CD instances:

 RUN Get-ChildItem C:\transforms\solution\*\*\platform\web.config.xdt | ForEach-Object { & C:\tools\scripts\Invoke-XdtTransform.ps1 -Path .\web.config -XdtPath $_.FullName }

 

Path to the web.config.xdt can be different in your solution. As well as we have to make sure that transform files are copied from solution to the solution image in its build stage.

Don’t forget to pass the newly added environment variables to the container with your  image.