Overriding Web.config Settings with Environment Variables in Containerized ASP.NET Applications (with No Code Changes)
Friday, November 10, 2017
It's a common and useful practice to configure a container using environment variables. However, ASP.NET 4.7 and older versions use Web.config files for configuration, and there's no built-in mechanism to override those settings using environment variables. ASP.NET 4.7.1 adds an extensibility point called configuration builders that allows overriding configuration from sources including environment variables, but this requires us to upgrade the app to 4.7.1 and needs some minor changes to the codebase.
So I started thinking if it's possible to containerize ASP.NET applications in a way that allows us to override configuration using environment variables without the need to upgrade the app to 4.7.1 or change any code. This would make it much easier to lift and shift existing ASP.NET workloads to containers.
Configuration: the "Docker way"
One of the biggest benefits of using containers is the ability to wrap your application and its dependencies into an immutable image. This image is built once and deployed to different environments as it progresses through a continuous delivery pipeline. As the application is deployed, we pass a set of environment variables to the container to configure it for the given environment. This ensures that we deploy the exact same application to production as the one tested in other environments.
The use of environment variables also helps keep secrets out of container images.
Extending the microsoft/aspnet base image to use environment variables
The easiest way to containerize an ASP.NET application is using the microsoft/aspnet base image. It's as easy as a 3-line Dockerfile:
FROM microsoft/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .
Here, we're using the 4.7.1 Windows Server Core 1709 base image, but there are images for other versions as well and they all work the same way.
We need to create a modified version of the microsoft/aspnet image that allows overriding settings with environment variables. The technique to do this is fairly simple:
- Create a script (Set-WebConfigSettings.ps1) that reads environment variables and overrides configuration in Web.config by modifying the file
- Override the entry point of the microsoft/aspnet base image to call that script at container startup
Set-WebConfigSettings.ps1
This is the script that does the hard work:
param (
[string]$webConfig = "c:\inetpub\wwwroot\Web.config"
)
$doc = (Get-Content $webConfig) -as [Xml];
$modified = $FALSE;
$appSettingPrefix = "APPSETTING_";
$connectionStringPrefix = "CONNSTR_";
Get-ChildItem env:* | ForEach-Object {
if ($_.Key.StartsWith($appSettingPrefix)) {
$key = $_.Key.Substring($appSettingPrefix.Length);
$appSetting = $doc.configuration.appSettings.add | Where-Object {$_.key -eq $key};
if ($appSetting) {
$appSetting.value = $_.Value;
Write-Host "Replaced appSetting" $_.Key $_.Value;
$modified = $TRUE;
}
}
if ($_.Key.StartsWith($connectionStringPrefix)) {
$key = $_.Key.Substring($connectionStringPrefix.Length);
$connStr = $doc.configuration.connectionStrings.add | Where-Object {$_.name -eq $key};
if ($connStr) {
$connStr.connectionString = $_.Value;
Write-Host "Replaced connectionString" $_.Key $_.Value;
$modified = $TRUE;
}
}
}
if ($modified) {
$doc.Save($webConfig);
}
Startup.ps1
This is the script that runs at container startup. The second command is the entry point from the original Docker image that monitors the w3svc
service.
C:\aspnet-startup\Set-WebConfigSettings.ps1 -webConfig c:\inetpub\wwwroot\Web.config
C:\ServiceMonitor.exe w3svc
Dockerfile
This Dockerfile creates the modified version of the image, calling the startup script.
FROM microsoft/aspnet:4.7.1-windowsservercore-1709
RUN md c:\aspnet-startup
COPY . c:/aspnet-startup
ENTRYPOINT ["powershell.exe", "c:\\aspnet-startup\\Startup.ps1"]
If you want to try this out, I've published the image to Docker Hub at anthonychu/aspnet (the 1709 image requires Windows 10 Fall Creators update or Windows Server version 1709). This image also includes the ability to apply web.config transformations, as described in this article.
Using the new image
To test this out, we'll create an ASP.NET WebForms project and add an app settings and a connection string to Web.config:
<configuration>
<appSettings>
<add key="PageTitle" value="Hello World" />
</appSettings>
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\Foo.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- ... -->
</configuration>
To containerize the application, first we need to build it. Then we build the container image using this Dockerfile. It's the same Dockerfile as we would normally use, except it uses the base image we created earlier:
FROM anthonychu/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .
Start a container normally and see the original Web.config values displayed:
PS> docker run -d -p 80:80 sample-aspnet-4x
Now, start another container, this time we'll pass environment variables to override the Web.config values. To override an app setting, prefix the environment variable with APPSETTING_
; to override a connection, prefix it with CONNSTR_
:
PS> docker run -d -p 80:80 `
-e APPSETTING_PageTitle="Foo" -e CONNSTR_DefaultConnection="From Environment!!" `
sample-aspnet-4x
Source code
https://github.com/anthonychu/aspnet-env-docker
Also check out my article on how to apply web.config transforms on containerized ASP.NET applications.
It's a common and useful practice to configure a container using environment variables. However, ASP.NET 4.7 and older versions use Web.config files for configuration, and there's no built-in mechanism to override those settings using environment variables. ASP.NET 4.7.1 adds an extensibility point called configuration builders that allows overriding configuration from sources including environment variables, but this requires us to upgrade the app to 4.7.1 and needs some minor changes to the codebase.
So I started thinking if it's possible to containerize ASP.NET applications in a way that allows us to override configuration using environment variables without the need to upgrade the app to 4.7.1 or change any code. This would make it much easier to lift and shift existing ASP.NET workloads to containers.
Configuration: the "Docker way"
One of the biggest benefits of using containers is the ability to wrap your application and its dependencies into an immutable image. This image is built once and deployed to different environments as it progresses through a continuous delivery pipeline. As the application is deployed, we pass a set of environment variables to the container to configure it for the given environment. This ensures that we deploy the exact same application to production as the one tested in other environments.
The use of environment variables also helps keep secrets out of container images.
Extending the microsoft/aspnet base image to use environment variables
The easiest way to containerize an ASP.NET application is using the microsoft/aspnet base image. It's as easy as a 3-line Dockerfile:
FROM microsoft/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .
Here, we're using the 4.7.1 Windows Server Core 1709 base image, but there are images for other versions as well and they all work the same way.
We need to create a modified version of the microsoft/aspnet image that allows overriding settings with environment variables. The technique to do this is fairly simple:
- Create a script (Set-WebConfigSettings.ps1) that reads environment variables and overrides configuration in Web.config by modifying the file
- Override the entry point of the microsoft/aspnet base image to call that script at container startup
Set-WebConfigSettings.ps1
This is the script that does the hard work:
param (
[string]$webConfig = "c:\inetpub\wwwroot\Web.config"
)
$doc = (Get-Content $webConfig) -as [Xml];
$modified = $FALSE;
$appSettingPrefix = "APPSETTING_";
$connectionStringPrefix = "CONNSTR_";
Get-ChildItem env:* | ForEach-Object {
if ($_.Key.StartsWith($appSettingPrefix)) {
$key = $_.Key.Substring($appSettingPrefix.Length);
$appSetting = $doc.configuration.appSettings.add | Where-Object {$_.key -eq $key};
if ($appSetting) {
$appSetting.value = $_.Value;
Write-Host "Replaced appSetting" $_.Key $_.Value;
$modified = $TRUE;
}
}
if ($_.Key.StartsWith($connectionStringPrefix)) {
$key = $_.Key.Substring($connectionStringPrefix.Length);
$connStr = $doc.configuration.connectionStrings.add | Where-Object {$_.name -eq $key};
if ($connStr) {
$connStr.connectionString = $_.Value;
Write-Host "Replaced connectionString" $_.Key $_.Value;
$modified = $TRUE;
}
}
}
if ($modified) {
$doc.Save($webConfig);
}
Startup.ps1
This is the script that runs at container startup. The second command is the entry point from the original Docker image that monitors the w3svc
service.
C:\aspnet-startup\Set-WebConfigSettings.ps1 -webConfig c:\inetpub\wwwroot\Web.config
C:\ServiceMonitor.exe w3svc
Dockerfile
This Dockerfile creates the modified version of the image, calling the startup script.
FROM microsoft/aspnet:4.7.1-windowsservercore-1709
RUN md c:\aspnet-startup
COPY . c:/aspnet-startup
ENTRYPOINT ["powershell.exe", "c:\\aspnet-startup\\Startup.ps1"]
If you want to try this out, I've published the image to Docker Hub at anthonychu/aspnet (the 1709 image requires Windows 10 Fall Creators update or Windows Server version 1709). This image also includes the ability to apply web.config transformations, as described in this article.
Using the new image
To test this out, we'll create an ASP.NET WebForms project and add an app settings and a connection string to Web.config:
<configuration>
<appSettings>
<add key="PageTitle" value="Hello World" />
</appSettings>
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\Foo.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- ... -->
</configuration>
To containerize the application, first we need to build it. Then we build the container image using this Dockerfile. It's the same Dockerfile as we would normally use, except it uses the base image we created earlier:
FROM anthonychu/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .
Start a container normally and see the original Web.config values displayed:
PS> docker run -d -p 80:80 sample-aspnet-4x
Now, start another container, this time we'll pass environment variables to override the Web.config values. To override an app setting, prefix the environment variable with APPSETTING_
; to override a connection, prefix it with CONNSTR_
:
PS> docker run -d -p 80:80 `
-e APPSETTING_PageTitle="Foo" -e CONNSTR_DefaultConnection="From Environment!!" `
sample-aspnet-4x
Source code
https://github.com/anthonychu/aspnet-env-docker
Also check out my article on how to apply web.config transforms on containerized ASP.NET applications.