Documentation
fsds/dplx-azure-app-config-implementation-plan.md
Centralized Configuration with Azure App Configuration
Overview
Migrate all configuration from distributed appsettings.json files to Azure App Configuration. The AppHost's appsettings.json becomes the single source of truth for local development, automatically seeding the App Config emulator at startup.
Design Decisions
- Secrets: Azure Key Vault (postgres-kv) - same vault for all secrets
- Logging: All config in App Config (complete elimination of component appsettings.json)
- Key Structure: Flat with component prefixes (
Iot:Tn360:ApiKey,Bbu:Mqtt:Host) - Local Dev: App Config emulator seeded from AppHost config
- Azure: Real App Config service with Key Vault references
Architecture
Local Development:
AppHost appsettings.json ─────► App Config Emulator ─────► Components
(source of truth) (seeded at startup) (read config)
Azure Deployment:
Azure App Configuration ─────► Key Vault (secrets) ─────► Components
(pre-populated) (postgres-kv)
Implementation Stages
Stage 1: Fix Azure.Core 409 Logging Noise (Quick Win)
Add logging filter to suppress expected 409 "ContainerAlreadyExists" responses.
Files:
engines/iot/src/Acsis.Dynaplex.Engines.Iot/appsettings.json- Add"Azure.Core": "Error"engines/bbu/src/Acsis.Dynaplex.Engines.Bbu/appsettings.json- Add"Azure.Core": "Error"engines/file-service/src/Acsis.Dynaplex.Engines.FileService/appsettings.json- Add"Azure.Core": "Error"
Change:
"Logging": {
"LogLevel": {
"Azure.Core": "Error"
}
}
Stage 2: App Configuration Seeder (Infrastructure)
Create seeder that populates App Config emulator from AppHost configuration.
New File: strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/AppConfigurationSeeder.cs
public class AppConfigurationSeeder {
private readonly IConfiguration _configuration;
private readonly string _keyVaultName;
public async Task SeedAsync(string connectionString, CancellationToken ct) {
var client = new ConfigurationClient(connectionString);
// Flatten hierarchical config to colon-separated keys
// Convert "kv:{secret-name}" values to Key Vault references
// Upsert all values to App Config
}
}
Package to add: Azure.Data.AppConfiguration in Orchestration project
Stage 3: Lifecycle Hook for Seeding
Update AddDynaplexAppConfiguration() to seed emulator when ready.
File: strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/DynaplexInfrastructureExtensions.cs
public static IDistributedApplicationBuilder AddDynaplexAppConfiguration(...) {
// ... existing emulator setup ...
if (!context.IsAzurePublish) {
// Subscribe to ResourceReadyEvent to seed after emulator starts
builder.Eventing.Subscribe<ResourceReadyEvent>(
context.AppConfig.Resource,
async (evt, ct) => {
var seeder = new AppConfigurationSeeder(builder.Configuration);
var connStr = await context.AppConfig.Resource.GetConnectionStringAsync(ct);
await seeder.SeedAsync(connStr, ct);
});
}
}
Stage 4: ServiceDefaults Integration (Component Side)
Add automatic App Config integration to AddServiceDefaults().
File: strata/service-defaults/src/Acsis.Dynaplex.Strata.ServiceDefaults/Extensions.cs
New method:
public static TBuilder AddAcsisAppConfiguration<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder {
var configConn = builder.Configuration.GetConnectionString("config");
if (string.IsNullOrEmpty(configConn)) return builder;
var componentName = ExtractComponentName(Assembly.GetEntryAssembly());
builder.AddAzureAppConfiguration("config",
configureOptions: options => {
options.Select("Logging:*"); // Shared
options.Select($"{componentName}:*")
.TrimKeyPrefix($"{componentName}:"); // Component-specific
options.ConfigureKeyVault(kv => {
kv.SetCredential(new DefaultAzureCredential());
});
});
// Suppress 409 noise
builder.Logging.AddFilter("Azure.Core", LogLevel.Error);
return builder;
}
Update AddServiceDefaults():
public static TBuilder AddServiceDefaults<TBuilder>(...) {
builder.AddAcsisAppConfiguration(); // Add this first
builder.ConfigureOpenTelemetry();
// ... rest unchanged
}
Stage 5: Component Wire-up
Update component registration to automatically reference App Config.
File: strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/DynaplexComponentExtensions.cs
In AddComponent<T>():
if (context.AppConfig != null) {
component.WithReference(context.AppConfig);
}
Stage 6: Master Configuration Structure
Restructure AppHost's appsettings.json as master config.
File: projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Azure.Core": "Error"
}
},
"Iot": {
"Tn360": {
"BaseUrl": "https://api-us.telematics.com/v1",
"ApiKey": "kv:tn360-api-key",
"RetryPolicy": { "MaxAttempts": 3 }
},
"DatalogicBlobProcessor": {
"Enabled": true,
"ContainerName": "bbu-image",
"PollingIntervalSeconds": 15
}
},
"Bbu": {
"TenantId": "00000000-0000-0000-0000-000000000001",
"Mqtt": {
"Enabled": true,
"BrokerHost": "i5122f12.ala.us-east-1.emqxsl.com",
"BrokerPort": 8883,
"Username": "kv:bbu-mqtt-username",
"Password": "kv:bbu-mqtt-password"
}
},
"Identity": {
"Issuer": "acsis-identity",
"Audience": "acsis-api",
"ExpirationSpan": 8,
"RsaPrivateKey": "kv:identity-rsa-private-key",
"RsaPublicKey": "kv:identity-rsa-public-key"
}
}
Values prefixed with kv: are converted to Key Vault references during seeding.
Stage 7: Remove Component appsettings.json Files
After migration, delete:
engines/iot/src/Acsis.Dynaplex.Engines.Iot/appsettings.jsonengines/bbu/src/Acsis.Dynaplex.Engines.Bbu/appsettings.jsonengines/events/src/Acsis.Dynaplex.Engines.Events/appsettings.jsonengines/catalog/src/Acsis.Dynaplex.Engines.Catalog/appsettings.json- All other component appsettings.json files
Stage 8: Clean Up IoT Manual Integration
Remove manual App Config code from IoT component.
File: engines/iot/src/Acsis.Dynaplex.Engines.Iot/Program.cs
Remove lines 33-38:
// DELETE:
var appConfigConnection = builder.Configuration.GetConnectionString("config");
if (!string.IsNullOrEmpty(appConfigConnection))
{
builder.AddAzureAppConfiguration("config");
}
Now handled automatically by AddServiceDefaults().
Critical Files Summary
| File | Change |
|---|---|
DynaplexInfrastructureExtensions.cs |
Add seeding lifecycle hook |
DynaplexComponentExtensions.cs |
Auto-reference App Config |
ServiceDefaults/Extensions.cs |
Add AddAcsisAppConfiguration() |
AppHost appsettings.json |
Restructure as master config |
Component Program.cs files |
Remove manual App Config code |
Component appsettings.json files |
Delete after migration |
Key Vault Secrets to Add
For local dev, add to user-secrets. For Azure, add to postgres-kv:
tn360-api-keybbu-mqtt-usernamebbu-mqtt-passwordidentity-rsa-private-keyidentity-rsa-public-keyevents-eventgrid-sastokenokta-client-secretazure-ad-client-secret
Execution Order
- Quick Win: Fix Azure.Core logging noise (Stage 1)
- Infrastructure: Create seeder + lifecycle hook (Stages 2-3)
- ServiceDefaults: Add automatic integration (Stage 4)
- Orchestration: Update component wire-up (Stage 5)
- Migration: Move config to AppHost, delete component configs (Stages 6-8)
- Testing: Verify local and Azure deployments