Anthony Liriano
Kubernetes ConfigMap & .NET 5 AppSettings.Json
Overview
A Kubernetes ConfigMap is used to store non-confidential data in key-value pairs. The ConfigMap can then be consumed as environment variables, command-line arguments, or as configuration files in a volume. ConfigMaps can also be referenced by multiple pods.
Appsettings.Json is the default configuration file used in .NET to store all of the application settings.
As we’re starting to adopt Kubernetes, we want to start using ConfigMaps to store application configuration. By using ConfigMaps, we hope we’ll be able to reuse our application configuration files and reduce dupplication.
Currently, any changes made to ConfigMap would require the pod to be restarted in order for the application to read the new changes. There’s an enhancement being considered to allow reloadOnChange option to work in Linux enviroments, feature being tracked here: Github Issue #36091
To view or reference the .NET Project created for this demonstration please go to Github Repository
Configuring The .NET 5 Project
For this demonstration, I created an empty ASP.NET CORE Web API Project targetting the .NET 5 Framework.
I modified the Program.cs file so that the application is not using the default web host builder and recognizes the appsettings.k8s.json file. Here’s what my HostBuilder method looks like like
public static IHostBuilder CreateHostBuilder(string[] args)
{
return new HostBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseKestrel();
}).ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Trace);
}).ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
config.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: false);
config.AddJsonFile("appsettings.k8s.json", optional: true, reloadOnChange: false);
});
}
To validate the changes have worked, I’m exposing the Appsettings file via a controller. I created the AppSettingsController.cs with a simple HTTP GET method.
[Route("api/settings")]
[ApiController]
public class AppSettingsController : ControllerBase
{
private readonly IConfiguration _configuration;
public AppSettingsController(IConfiguration config)
{
_configuration = config;
}
[HttpGet]
public IActionResult GetConfigurationSettings()
{
return Ok(_configuration.GetSection("AppConfiguration").GetChildren());
}
}
Dockerfile
Containarize the application
FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS base
ENV ASPNETCORE_URLS http://+:83
EXPOSE 83
# Set TimeZone
ENV TZ America/New_York
FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build
WORKDIR /src
COPY NET5-AppSettings-ConfigMap-Demo.csproj NET5-AppSettings-ConfigMap-Demo/
RUN dotnet restore NET5-AppSettings-ConfigMap-Demo/NET5-AppSettings-ConfigMap-Demo.csproj --source https://api.nuget.org/v3/index.json
WORKDIR /src/NET5-AppSettings-ConfigMap-Demo
COPY . .
RUN dotnet build NET5-AppSettings-ConfigMap-Demo.csproj -c Release -o /app
FROM build AS publish
RUN dotnet publish NET5-AppSettings-ConfigMap-Demo.csproj -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "NET5-AppSettings-ConfigMap-Demo.dll"]
ConfigMap
There are several ways we can consume ConfigMaps inside pods. In this scenario we’ll be adding the ConfigMap as a readonly file. In order for this to work, we’ll want to structure the Configmap so that the data propery contains both the name of the file and the contents?
apiVersion: v1
kind: ConfigMap
metadata:
name: appsettings-demo
data:
appsettings.k8s.json: |-
{
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Error",
"Microsoft": "Error"
}
},
"AppConfiguration": {
"DatabaseHost": "localhost",
"ThirdPartyAPIBaseURL" : "https://local.somewhere.com/"
}
}
Deployment
After the ConfigMap is created we need a way to reference the ConfigMap so that the container can read its contents. In the Kubernetes Deployment, we’ll want to include two additional properties volumeMounts and volumes.
Volume - In short words, this is can be thought of a directory that contains files. There are different types of volumes, here we’re using the configMap type. In a deployment, the volume is specified in .spec.volumes
volumes:
- name: appsettings-demo-volume
configMap:
name: appsettings-demo
Volume Mounts - Now that we specified the volume, we’ll want to indicate where we want to mount this volume (“directory”).. In a deployment, the volume is specified in .spec.containers[*].volumeMounts
volumeMounts:
- name: appsettings-demo-volume
mountPath: /app/appsettings.k8s.json
subPath: appsettings.k8s.json
Here’s is what the Deployment should look like..
kind: Deployment
apiVersion: apps/v1
metadata:
name: appsettings-demo
namespace: default
labels:
k8s-app: appsettings-demo
spec:
replicas: 1
selector:
matchLabels:
k8s-app: appsettings-demo
template:
metadata:
name: appsettings-demo
labels:
k8s-app: appsettings-demo
spec:
volumes:
- name: appsettings-demo-volume
configMap:
name: appsettings-demo
containers:
- name: appsettings-demo
image: 'anthonylir/dotnet-appsettings-configmap-demo:1.0.0'
volumeMounts:
- name: appsettings-demo-volume
mountPath: /app/appsettings.k8s.json
subPath: appsettings.k8s.json
imagePullPolicy: Always
imagePullSecrets:
- name: regcred
Verifying
To keep things simple, I am testing by executing within the container and running the curl commands below to verify the response contains the contents of the ConfigMap.
Execute into the container
kubectl exec --stdin --tty appsettings-demo-69cf9684dc-6w28h -- /bin/bash
Run the curl command inside the container
curl -X GET localhost:83/api/settings
Output
[
{
"key":"DatabaseHost",
"path":"AppConfiguration:DatabaseHost",
"value":"prod.somedb.com"
},
{
"key":"ThirdPartyAPIBaseURL",
"path":"AppConfiguration:ThirdPartyAPIBaseURL",
"value":"https://prod.somewhere.com/"
}
]
© 2022 anthonyliriano.com