Skip to content

Environment Configuration (envdef)

Under the ENV Management Service architecture, each service owns the shape of its environment configuration in its own repository as a local definition file (envdef.yaml). The service never resolves this file itself: it sends the raw definition to the ENV Management Service, which overlays deployment context, resolves secret references through the configured secret provider, applies optional central overrides, and returns a fully resolved environment map.

This page defines the local def file, its reference syntax, and the configuration precedence. For the key-naming rules (the AC_ prefix, DOMAIN_TYPE component names, standard keywords) see Environment Variables.

The envdef.yaml File

Location

envdef.yaml lives in the service's project root, next to appsettings.json (the place a developer instinctively looks for configuration). It is versioned with the service and ships in the container image.

src/Store.Api/
  appsettings.json          # static, deployment-invariant defaults
  envdef.yaml               # env definition (this file)
  Program.cs
  Store.Api.csproj

An optional per-environment overlay envdef.{Environment}.yaml (e.g. envdef.Production.yaml) sits beside the base file. {Environment} matches AC_ENVIRONMENT (Development, Testing, Staging, Production).

The same file, in the same location, is used by Node.js services and is read by the ENV Agent for third-party components (Keycloak, etc.). The format is language-agnostic on purpose.

Root Structure

The file is a flat key -> value map. The value's syntax alone determines its source class (static / template / secret) - there is no separate type field, which keeps the file terse and hand-editable.

# envdef.yaml
version: 1
service: store-api          # component identity; default namespace for ${secret:./...}

def:
  # static - literal, no interpolation. Own properties: no component (see Environment Variables)
  AC_ENVIRONMENT: Production
  AC_PORT: 8080
  AC_CACHE_ENABLED: true

  # template - references deployment context
  AC_EXTERNAL_URL: '${context:root_external}/store/api'
  AC_INTERNAL_URL: '${context:cluster_domain}/store'

  # secret ref - resolved by the secret provider (OpenBao). The secret path stays
  # service-scoped for authorization even though the variable name carries no component
  AC_DB_CONNECTION_STRING: '${secret:store-api/db_connection}'
  AC_JWT_SIGNING_KEY: '${secret:./jwt_signing_key}'   # ./ = this service

  # composition - reference another resolved AC_ key
  AC_HEALTH_URL: '${env:AC_INTERNAL_URL}/health'

  # cross-cutting dependency - component names the dependency, not this service, scheme-less by convention (see Kafka's own bootstrap.servers)
  AC_KAFKA_BOOTSTRAP_SERVERS: '${context:kafka_bootstrap_servers}'

Root fields:

Field Required Meaning
version yes Schema version, for forward compatibility. Currently 1.
service yes The service/component identity. Used as the default secret namespace, so ${secret:./key} means "this service".
def yes The map of AC_ keys to their values or references.

An overlay file (envdef.{Environment}.yaml) has the same shape and is merged key by key: a key present in the overlay replaces the base value; keys not in the overlay are inherited.

Reference Syntax

Any value that is not a plain literal uses the ${...} interpolation form. The ${...} form is chosen over a bare {...} because a plain YAML scalar starting with { is parsed as a flow mapping (a parse error), while ${...} starts with $ and is safe; it is also familiar from shell.

References are namespaced. The namespace is not just routing - it encodes the resolution backend, the sensitivity of the value (whether it must be masked in logs and reports), the authorization scope, and the audit source class. Do not collapse the namespaces into one keyword.

Reference Resolves from Sensitive Example
${secret:<service>/<path>} The configured secret provider (OpenBao by default) Yes - masked ${secret:store-api/db_connection}
${context:<name>} Per-install deployment context (globals set once per deployment) No ${context:rabbitmq_url}
${env:<AC_KEY>} Another already-resolved AC_ key in the same resolution Inherits ${env:AC_INTERNAL_URL}

Secret references

Secret references are path-structured: ${secret:<service>/<path>}. The first segment is the service and acts as the authorization boundary - it maps directly to a secret-provider path and a per-service least-privilege policy. The rest is the secret path within that service's namespace and may be nested.

AC_DB_CONNECTION_STRING: '${secret:store-api/db_connection}'
AC_DB_PASSWORD:          '${secret:store-api/db/password}'   # nested path
AC_JWT_SIGNING_KEY:      '${secret:./jwt_signing_key}'       # ./ = current service (root `service:`)

The variable name carries no component (these are the service's own secrets), but the secret path's first segment is always the real service identity — that's the authorization boundary, and it doesn't change based on how the variable is named.

${secret:store-api/db_connection} maps to the OpenBao path secret/data/store-api, field db_connection, governed by a policy such as:

path "secret/data/store-api/*" { capabilities = ["read"] }

A flat, underscore-joined form (${secret:store_api_db_connection}) is invalid: it is ambiguous about where the service name ends and it provides no authorization boundary.

Context references

Context values are per-install deployment globals (the ingress root, the cluster domain, shared infrastructure endpoints). They have no service dimension and no authorization scope, so they use a single flat namespace: ${context:<name>}. The set of available context names is defined and owned by the Platform team (see the well-known context values reference); a service cannot invent a new context name - adding one is a platform change.

AC_EXTERNAL_URL:  '${context:root_external}/store/api'
AC_EVENT_BUS_URI: '${context:rabbitmq_url}'

If a value carries credentials, it is a secret, not a context value.

Naming inside references

  • Names inside secret/context use lowercase snake_case: db_connection, rabbitmq_url, root_external.
  • The service segment of a secret reference matches the service identity (lowercase, aligned with the deployment/Kubernetes name): store-api.

Escaping

Two escaping layers are kept separate so they never interfere:

  1. YAML layer. Quote every value that contains ${...} with single quotes. In single-quoted YAML the backslash is literal and only '' escapes a quote, so the template syntax is passed through untouched. Rule: any line with ${...} is single-quoted.
  2. Template layer. To emit a literal $, write $$; the engine collapses $$ to $. This matches Docker Compose.
AC_PROMO_LABEL: 'Save $$5 today'    # resolves to: Save $5 today

YAML type pitfalls

YAML infers scalar types (the "Norway problem": NO becomes false; a leading-zero number like 08 is an octal error). The resolved output coerces every value to a string, but to avoid surprises in the source, quote ambiguous string values (country codes, versions, digit-only strings). Plain integers and booleans whose intent is obvious (8080, true) may stay unquoted.

Configuration Precedence

For AC_-prefixed keys, the ENV definition is authoritative. Resolved envdef values override appsettings.json, and even override a raw environment variable of the same name. This is deliberate:

  • Parity. The ENV Management Service must resolve the same input to the same output on every deployment target (Kubernetes/OpenShift, Podman/Docker, local). Letting an arbitrary environment variable silently override envdef breaks determinism.
  • Security. A stale or injected AC_ environment variable must not silently shadow a vault-resolved secret.
  • Explicit overrides. A developer overrides a value by editing the versioned envdef.Development.yaml, not by exporting a raw variable - the override is visible and tracked, not invisible shell state.

Precedence for AC_ keys, highest wins:

  1. envdef.{Environment}.yaml (resolved)
  2. envdef.yaml (resolved)
  3. Real OS environment variables (for non-AC_ keys only; in local dev these come from launchSettings.json)

appsettings.json and appsettings.{Environment}.json no longer hold AC_ values - anything environment- or install-specific (URLs, ports, addresses, secrets) moves to envdef.yaml. appsettings.json keeps only static, deployment-invariant defaults.

Non-AC_ framework/OS keys (DOTNET_*, PATH, ...) follow the standard .NET rules unchanged; envdef does not touch them. ASPNETCORE_ENVIRONMENT is not one of these - it is derived from AC_ENVIRONMENT, not set independently (see below).

AC_ENVIRONMENT is the one AC_ key excluded from the precedence rule above: it must exist as a real, bootstrap-time OS environment variable, because it is what selects which envdef.{Environment}.yaml overlay to load - it cannot itself come from a file it helps select. It is the single source of truth for "which environment is this" across every service and language in a deployment.

The role of launchSettings.json

launchSettings.json no longer overrides AC_ values. Its only role is to set AC_ENVIRONMENT, which selects the envdef.{Environment}.yaml overlay.

envdef is language-agnostic (Node.js services and the ENV Agent read the same file format and have no ASP.NET concept at all), so overlay selection is keyed off AC_ENVIRONMENT, not a framework-specific variable. In a .NET service, host startup reads AC_ENVIRONMENT and sets ASPNETCORE_ENVIRONMENT to the same value before appsettings.{Environment}.json is loaded - the framework's environment selection follows AC_ENVIRONMENT automatically instead of being configured separately.

Value Source Classification

Each AC_ key's source class is inferred from its value syntax, which is what the audit and the ENV Management Service use - no separate declaration is needed:

Class Value syntax Example
static Plain literal AC_PORT: 8080
template Contains ${context:...} (deployment context) '${context:root_external}/store/api'
secret ref Contains ${secret:...} '${secret:store-api/db_connection}'
deployment context A per-install global consumed by templates ${context:rabbitmq_url}

Authoring Checklist

Before adding a value to envdef.yaml, verify that:

  • The key follows the naming convention (AC_ prefix, correct suffix, and a component only when the value is about a different system — never this one).
  • A sensitive value uses ${secret:<service>/<path>}, never a literal.
  • A per-install value uses ${context:<name>} from the well-known set, not a hardcoded address.
  • Any value containing ${...} is single-quoted; a literal $ is written as $$.
  • Environment- or install-specific values live here, not in appsettings.json.