Yesterday I blogged about configmaps and how changing the configmap is not picked up by ASP.NET Core. In this post I want to dig deeper in why this is the case and suggest a possible solution.
The CreateDefaultBuilder() method in ASP.NET Core will load your appsettings.json file and monitor it for changes (see the reloadOnChange: true):
builder.ConfigureAppConfiguration((hostingContext, config) => | |
{ | |
var env = hostingContext.HostingEnvironment; | |
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | |
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); | |
}) |
Behind the scenes the reloadOnChange setting will monitor the config file for changes based on the last modified date. Seems logicalā¦
Letās continue to find out why this doesnāt work with Kubernetes. In Kubernetes a config map is mounted as a symlink:
kubectl exec -it <pod-name> -- bash
root@example-deployment-839f6c6546-c783b:/appg# ls -la
rwxrwxrwx 3 root root 4096 Mar 25 10:01 .
drwxr-xr-x 1 root root 4096 Mar 25 10:47 ..
drwxr-xr-x 2 root root 4096 Mar 25 10:01 ..2021_03_25_10_01_16.386067924
lrwxrwxrwx 1 root root 31 Mar 25 10:01 ..data -> ..2021_03_25_10_01_16.386067924
lrwxrwxrwx 1 root root 53 Mar 25 10:47 appsettings.json -> ..data/appsettings.json
Unfortunately when you update the config map, the last modified date doesnāt change, although the file reference itself is updated. This is a known issue as discussed here.
One suggested workaround for this problem is to use a a configuration provider that understand symlinks(notice that this solution only works on linux):
.ConfigureAppConfiguration(c => c.AddSymLinkJsonFile("appsettings.json", optional: true, reloadOnChange: true)); |
using Microsoft.Extensions.FileProviders; | |
using Mono.Unix; | |
using System.IO; | |
using System.Runtime.InteropServices; | |
namespace Microsoft.Extensions.Configuration | |
{ | |
internal static class JsonSymlinkConfigurationExtensions | |
{ | |
internal static void AddSymLinkJsonFile(this IConfigurationBuilder c, string configFileFullPath, bool optional, bool reloadOnChange) | |
{ | |
if (TryGetSymLinkTarget(fileInfo.PhysicalPath, out string targetPath)) | |
{ | |
if (Path.IsPathFullyQualified(targetPath) == false) | |
{ | |
targetPath = Path.GetFullPath(targetPath, Path.GetDirectoryName(configFileFullPath)); | |
} | |
c.AddJsonFile(new PhysicalFileProvider(Path.GetDirectoryName(targetPath)), Path.GetFileName(targetPath), optional, reloadOnChange); | |
} | |
else | |
{ | |
c.AddJsonFile(relativePath, optional, reloadOnChange); | |
} | |
} | |
private static bool TryGetSymLinkTarget(string path, out string target) | |
{ | |
target = null; | |
if (File.Exists(path) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | |
{ | |
var symbolicLinkInfo = new UnixSymbolicLinkInfo(path); | |
if (symbolicLinkInfo.IsSymbolicLink) | |
{ | |
target = symbolicLinkInfo.ContentsPath; | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
} |
Another possible solution is mentioned by Francisco Beltrao and uses the content itself to detect changes instead of the last modified date:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => | |
WebHost.CreateDefaultBuilder(args) | |
.ConfigureAppConfiguration(c => | |
{ | |
c.AddJsonFile(ConfigMapFileProvider.FromRelativePath(""), | |
"appsettings.json", | |
optional: true, | |
reloadOnChange: true); | |
}) | |
.UseStartup<Startup>(); |
The code required for this solution can be found in this GitHub repo: https://github.com/fbeltrao/ConfigMapFileProvider