Application Configuration Management Process
Overview
To ensure services can be reliably deployed and operated across multiple environments, we maintain a central registry of required configuration settings in /app-config.yaml. This file provides a clear inventory of what must be provisioned for the system to function correctly.
When to Add a Setting to /app-config.yaml
Developers MUST add a configuration key to /app-config.yaml if:
- The setting is required for the service to start or perform its core functions.
- The setting is environment-specific (e.g., URLs, connection strings, API keys) and does not have a "one-size-fits-all" default.
- The setting is an override of a default value that is expected to change in most production or production-like environments.
- The setting is security-sensitive (e.g., Auth Authority, Secret Keys). Note: only the key and description are added; never the value.
When to Exclude a Setting
It is acceptable to exclude a setting if:
- It has a sufficient default value that is unlikely to require an override in any deployed environment (e.g., internal cache timeouts, logging levels).
- The setting is strictly for local development and has no relevance in deployed environments.
Integration into Workflow
Pull Requests (PRs)
- Any PR that introduces a new required configuration setting MUST include an update to
/app-config.yaml. - PR reviewers should verify that the description for any new setting is clear and that no sensitive values or environment-specific defaults are included.
- Automated Check: CodeRabbit is configured to scan for newly introduced configuration keys in
appsettings.json,application.yml, and environment variable definitions. If new keys are detected without corresponding updates to/app-config.yaml, the PR will be flagged for correction and should be blocked until reconciled.
DevOps & Release Notes
/app-config.yamlserves as the primary hand-off artifact for DevOps during environment provisioning.- Release notes should highlight changes to
/app-config.yamlto ensure transparency across teams.
File Format and Schema
The /app-config.yaml follows a structured schema:
global Array
Contains settings shared across most or all services (e.g., Kafka connection, Database provider).
services Object
Contains service-specific settings, keyed by the service name.
Configuration Entry Fields:
key: The path to the configuration setting, in the notation of the runtime that reads it. See Key Notation by Runtime.description: A brief explanation of the setting's purpose and its impact on the system.required: (Optional, default: true) A row for this key must exist in every environment store. This is narrower than "the service needs it to run": a setting with a working default inappsettings.jsonorapplication.ymlshould berequired: falsewith that value recorded indefaultValue, because requiring it would mean provisioning one identical value into three stores. Reserverequired: truefor values that genuinely differ per environment, or that have no safe default. Enforced byScripts/AzureAppConfig/check_required_config.py. Note that the schema default is documentation only — JSON Schema defaults are annotation and are never applied during validation, so entries should staterequiredexplicitly.label: (Optional) The App Configuration label the entry is expected to carry.sensitive: (Optional, default: false) Whether the setting holds a secret. A sensitive entry must never also setdefaultValue; this is enforced.defaultValue: (Optional) A default value, for settings that ship with a working fallback inappsettings.jsonorapplication.yml.runtime: (Optional,dotnetorjava) Overrides the runtime inferred from the owning service. Needed only for Java-notation keys listed underglobal, which has no owning service.consumers: (Optional) The services that read this key, derived from the code inventory. Documentation only — it plays no part in resolution.
serviceMeta Object
Maps each service to two deployment facts: the App Configuration label the running service selects, and its runtime. Tooling reads this rather than hardcoding the mapping, so the label vocabulary lives in one place.
Label values are compiled into the services and must match exactly, including case and spaces. In .NET they are constants passed to AddExternalConfiguration(); in Java they are the label-filter in bootstrap.yml. Three are not simply the service name: AdminBFF uses LinkAdminBFF, AutomationUI uses Link Automation UI, and DataAcquisitionWorker is a distinct label from DataAcquisition.
How Services Resolve Keys
Label Resolution
Every service selects unlabeled keys first, then its own label. Both sets merge into one flat dictionary keyed by name alone — the label is a filter, not part of the resulting key — so a labeled row overrides the unlabeled row of the same name, for that service only. Other services, which do not select that label, continue to see the unlabeled value.
Two consequences follow, and they are asymmetric:
- Adding a labeled row on top of an unlabeled one is safe. This is the existing pattern for
AutoMigrate, theCORS:*family and the Serilogcomponentlabel. - Moving a key from unlabeled to labeled is not. Deleting the unlabeled row breaks every service that does not select that exact label. Only do this when precisely one service consumes the key.
Because labels are compiled in, a running service cannot be re-pointed at a different label without a redeploy.
Key Notation by Runtime
The two runtimes read different rows from the same store, in different notations. This is by design, not drift.
- .NET reads colon-delimited rows such as
KafkaConnection:BootstrapServers:0, matchingappsettings.jsonsection paths. - Java reads slash-prefixed rows such as
/spring/datasource/url. The Spring Cloud Azure provider appends*to the configuredkey-filterof/, giving a server-side filter of/*; it then strips the leading slash and converts remaining slashes to dots. So/spring/datasource/urlbecomes the Spring propertyspring.datasource.url. The colon-delimited .NET rows are structurally invisible to Java services.
The catalog records Java keys in the dotted Spring form (spring.datasource.url) while the store holds the slash form. These are two representations of one key; the transform between them is total and mechanical.
Values whose content_type is application/json are flattened by both providers. A single row /authentication holding {"anonymous": false, "authority": "..."} therefore supplies the properties authentication.anonymous and authentication.authority. Key Vault references are resolved automatically in both runtimes.
Precedence Over Environment Variables
A key present in App Configuration beats a container environment variable. This is the opposite of the usual expectation and is worth knowing before debugging a deployed service.
- Java: the Spring Cloud bootstrap property source defaults to
overrideSystemProperties=true, and nothing in this repository overrides it. - .NET:
AddAzureAppConfigurationis called after the host builder has already registered the environment-variable source, so the App Configuration source is appended last and wins.
Setting an environment variable on a pod will therefore be silently ignored for any key the store defines. Change the store instead.
Java Enablement
The checked-in bootstrap.yml for both Java services sets spring.cloud.azure.appconfiguration.enabled: false. That is the correct default for local and Docker runs, which take configuration from YAML and environment variables. Deployed environments set SPRING_CLOUD_AZURE_APPCONFIGURATION_ENABLED=true on the pods, so App Configuration is live in dev, test and qa. The endpoint and credentials are likewise supplied at deploy time and do not appear in this repository.
Security Warning
NEVER commit environment-specific values, passwords, or secrets to /app-config.yaml. This file is intended for documentation and schema enforcement only. Use secure secret management (e.g., Azure Key Vault) for actual values in deployed environments.
Relationships
flowchart LR nappconfig_1F4C6005["Design: Application Configuration Management Process"]