Managing .NET application configuration across environments in Kubernetes? ConfigMap and Secrets are the answer.
Step 1: Create ConfigMaps
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# Application settings
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AppSettings": {
"AppName": "OrderService",
"Environment": "Production",
"Version": "1.2.0"
},
"FeatureFlags": {
"EnableNewCheckout": "true",
"EnableBetaFeatures": "false"
}
}
# Simple key-value pairs
ASPNETCORE_ENVIRONMENT: "Production"
ASPNETCORE_URLS: "http://*:80"
LOG_LEVEL: "Information"
CACHE_TTL: "300"
MAX_RETRY_COUNT: "3"
# Connection strings (non-sensitive)
DatabaseHost: "postgres.production.svc.cluster.local"
DatabasePort: "5432"
DatabaseName: "ordersdb"
RedisHost: "redis.production.svc.cluster.local"
RedisPort: "6379"
Step 2: Create Secrets
# secret.yaml (base64 encoded) apiVersion: v1 kind: Secret metadata: name: app-secrets namespace: production type: Opaque data: # Base64 encoded values DatabasePassword: cGFzc3dvcmQxMjM= # password123 RedisPassword: cmVkaXNwYXNz # redispass ApiKey: c2VjcmV0LWtleS0xMjM= # secret-key-123 JwtSecret: dXByP1htR1ptQkZKT1pIUnJs # randomsecret --- # Or use stringData for plain text (converted to base64) apiVersion: v1 kind: Secret metadata: name: app-secrets-plain namespace: production type: Opaque stringData: DatabasePassword: password123 RedisPassword: redispass ApiKey: secret-key-123 # This is easier to read and manage
Step 3: Mount ConfigMap as File
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: app
image: myregistry/order-service:latest
ports:
- containerPort: 80
# Mount ConfigMap as file
volumeMounts:
- name: app-config-volume
mountPath: /app/appsettings.Production.json
subPath: appsettings.json
- name: app-secrets-volume
mountPath: /app/secrets.json
subPath: secrets.json
# Use environment variables
env:
- name: ASPNETCORE_ENVIRONMENT
valueFrom:
configMapKeyRef:
name: app-config
key: ASPNETCORE_ENVIRONMENT
- name: ConnectionStrings__Database
valueFrom:
configMapKeyRef:
name: app-config
key: DatabaseHost
- name: ConnectionStrings__Redis
valueFrom:
configMapKeyRef:
name: app-config
key: RedisHost
# Use secrets as environment variables
- name: DatabasePassword
valueFrom:
secretKeyRef:
name: app-secrets
key: DatabasePassword
- name: RedisPassword
valueFrom:
secretKeyRef:
name: app-secrets
key: RedisPassword
- name: JwtSecret
valueFrom:
secretKeyRef:
name: app-secrets
key: JwtSecret
# Volumes
volumes:
- name: app-config-volume
configMap:
name: app-config
- name: app-secrets-volume
secret:
name: app-secrets
Step 4: Update .NET to Use Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Load configuration from multiple sources
var configBuilder = new ConfigurationBuilder()
.SetBasePath(builder.Environment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddJsonFile("secrets.json", optional: true) // Mounted from secrets
.AddEnvironmentVariables()
.Build();
builder.Configuration.AddConfiguration(configBuilder);
// Build connection string from ConfigMap values
var dbHost = builder.Configuration["DatabaseHost"];
var dbName = builder.Configuration["DatabaseName"];
var dbPassword = builder.Configuration["DatabasePassword"];
var connectionString = $"Host={dbHost};Database={dbName};Username=appuser;Password={dbPassword}";
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
// Use Redis from ConfigMap
var redisHost = builder.Configuration["RedisHost"];
var redisPassword = builder.Configuration["RedisPassword"];
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = $"{redisHost},password={redisPassword}";
});
// Use Feature Flags
var enableNewCheckout = builder.Configuration["FeatureFlags:EnableNewCheckout"] == "true";
if (enableNewCheckout)
{
builder.Services.AddScoped<INewCheckoutService, NewCheckoutService>();
}
Step 5: Dynamic Configuration Updates
// Watch ConfigMap changes
public class ConfigMapWatcher : BackgroundService
{
private readonly IConfiguration _configuration;
private readonly ILogger<ConfigMapWatcher> _logger;
private readonly IServiceProvider _services;
private readonly FileSystemWatcher _watcher;
public ConfigMapWatcher(IConfiguration configuration, ILogger<ConfigMapWatcher> logger, IServiceProvider services)
{
_configuration = configuration;
_logger = logger;
_services = services;
_watcher = new FileSystemWatcher("/app")
{
Filter = "appsettings.*.json",
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime
};
_watcher.Changed += OnConfigChanged;
_watcher.Created += OnConfigChanged;
}
private async void OnConfigChanged(object sender, FileSystemEventArgs e)
{
_logger.LogInformation("Configuration file {FileName} changed, reloading...", e.Name);
// Reload configuration
var reload = _configuration as IConfigurationRoot;
reload?.Reload();
_logger.LogInformation("Configuration reloaded");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(Timeout.Infinite, stoppingToken);
}
}
Step 6: Deploy and Manage
# Apply configurations
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f deployment.yaml
# Check ConfigMaps
kubectl get configmaps -n production
kubectl describe configmap app-config -n production
kubectl get configmap app-config -n production -o yaml
# Check Secrets
kubectl get secrets -n production
kubectl describe secret app-secrets -n production
kubectl get secret app-secrets -n production -o yaml
# Update ConfigMap (without restart)
kubectl edit configmap app-config -n production
# Trigger rollout after ConfigMap change
kubectl patch deployment order-service -n production \
-p '{"spec":{"template":{"metadata":{"annotations":{"configHash":"'$(date +%s)'"}}}}}'
# Update Secrets
kubectl create secret generic app-secrets \
--from-literal=DatabasePassword=newpassword \
--dry-run=client -o yaml | kubectl apply -f -
# Rollback if needed
kubectl rollout undo deployment/order-service -n production
# Debug configuration
kubectl exec -it order-service-xxxxx -n production -- cat /app/appsettings.Production.json
kubectl exec -it order-service-xxxxx -n production -- printenv
