Skip to main content
Version: 0.9.12

vars.yaml schema

platform v0.9.11verified 2026-05-14

vars.yaml is the manifest every service ships. It declares every environment variable the service expects at runtime, where the value should come from, and what should happen if it's missing. fetch-env.sh reads this file on every restart and resolves each entry to a concrete value before the containers start.

The manifest lives at /opt/services/<service>/vars.yaml after fetch-config syncs the bundle.

Top-level shape

service: api

variables:
- name: ENVIRONMENT
source: local
required: true

- name: REDIS_HOST
required: true
containers: [telapi]

- name: REDIS_PASSWORD
required: true
sensitive: true
containers: [telapi]

- name: LOG_LEVEL
default: 'info'
containers: [telapi]
Top-level fieldTypeDescription
servicestringThe service identifier. Combined with NAMESPACE it forms the SSM and Secrets Manager paths (/<NS>/<service>/...).
variableslist of objectVariable declarations (see below).

Variable fields

FieldTypeDescription
namestringEnvironment variable name (required).
requiredboolUnconditionally required. Deployment fails (fetch-env.sh aborts) if missing from every source.
required_whenstringConditionally required — format OTHER_VAR=value. Example: required_when: AUTH_WITH_MICROSOFT=true makes the variable required only when AUTH_WITH_MICROSOFT resolves to true.
defaultstringDefault value when not provided via SSM / SM / bootstrap.
sensitiveboolDocuments intent. Sensitive variables are expected to live in Secrets Manager. Seed scripts and provisioning workflows use this flag to decide whether to create an SSM parameter or a SM secret. fetch-env.sh itself does not branch on this flag.
sourcestringlocal = comes from the bootstrap /opt/deployment/.env, not fetched from SSM / SM. Default (omitted) = remote.
scopestringinit = consumed by init.sh on the host, not passed to containers. Documentation hint; not enforced.
containerslistWhich docker-compose.yaml services should receive this variable. Documentation hint used by show-env.sh and seed tooling; the actual injection is via ${VAR} substitution in docker-compose.yaml.
secretobjectValue comes from an existing Secrets Manager secret (e.g. an RDS master user secret) rather than from this service's auto-discovered SM bundle. See external-secret pattern below.
composeobjectThe value is built at runtime from a template after every other variable is resolved. See composition pattern below.

fetch-env.sh is permissive about unknown fields — comments and reminder strings can sit alongside the schema fields.

Worked examples

Bootstrap-only variable

Set by cloud-init, never fetched from SSM / SM:

- name: ENVIRONMENT
source: local
required: true

- name: HOSTNAME
source: local
required: true

- name: ECR_REGISTRY
source: local
required: true

fetch-env.sh reads these from /opt/deployment/.env. Mark as source: local whenever a value must come from the host — otherwise SSM (when populated) wins after eval $ENV_EXPORTS, which is a footgun for things like NAMESPACE itself.

Non-sensitive variable from SSM

- name: REDIS_HOST
required: true
containers: [telapi]

- name: LOG_LEVEL
default: 'info'
containers: [telapi]

fetch-env.sh looks up /${NAMESPACE}/${SERVICE}/REDIS_HOST in SSM. If absent and default: is set, the default is used; otherwise (with required: true) the script aborts.

Sensitive variable from Secrets Manager

- name: REDIS_PASSWORD
required: true
sensitive: true
containers: [telapi]

fetch-env.sh calls secretsmanager:list-secrets filtered by name-prefix ${NAMESPACE}/${SERVICE}/, fetches every match, parses each as JSON, and merges its keys. REDIS_PASSWORD is expected to be a top-level key in one of those JSON objects (typically ${NAMESPACE}/<service>/secrets).

Customers can split secrets however they like — fetch-env.sh discovers everything under the prefix:

# Single secret (default pattern):
voiceai/staging/api/secrets -> { "DATABASE_URL": "...", "REDIS_PASSWORD": "...", "JWT_SECRET": "..." }

# Multiple secrets (alternative pattern):
voiceai/staging/api/db-creds -> { "DATABASE_URL": "..." }
voiceai/staging/api/cache-creds -> { "REDIS_PASSWORD": "..." }
voiceai/staging/api/auth-creds -> { "JWT_SECRET": "..." }

Conditional requirement

- name: AUTH_WITH_MICROSOFT
default: 'false'
containers: [telweb]

- name: MICROSOFT_ENTRA_ID_CLIENT_ID
required_when: AUTH_WITH_MICROSOFT=true
containers: [telweb]

- name: MICROSOFT_ENTRA_ID_CLIENT_SECRET
required_when: AUTH_WITH_MICROSOFT=true
sensitive: true
containers: [telweb]

The two Microsoft variables only have to be set when the toggle is on. Otherwise fetch-env.sh skips the requirement check.

External-secret pattern

Use when the credential is not in ${NAMESPACE}/${SERVICE}/secrets but in another secret AWS owns — typically the JSON secret RDS creates for the cluster master user. The bootstrap places the ARN of that secret in /opt/deployment/.env:

- name: DB_MASTER_SECRET_ARN
source: local
required: true

- name: DATABASE_PASSWORD
required: true
sensitive: true
containers: [telapi]
secret:
arn_from: DB_MASTER_SECRET_ARN
type: json
key: password

- name: DATABASE_USERNAME
required: true
sensitive: true
containers: [telapi]
secret:
arn_from: DB_MASTER_SECRET_ARN
type: json
key: username

fetch-env.sh calls aws secretsmanager get-secret-value --secret-id $DB_MASTER_SECRET_ARN, parses the result as JSON, and pulls the named key. Set secret.type: plaintext when the secret is a single string instead of JSON.

Seed tooling skips SSM / SM creation for secret.arn_from entries — there is no per-service placeholder to populate. See Managed database secrets for the full pattern including a composed DATABASE_URL.

Composition pattern

Apps usually expect a single DATABASE_URL rather than separate parts. After every other variable has resolved, fetch-env.sh runs compose.template substitutions:

- name: DATABASE_HOST
required: true
containers: [telapi]

- name: DATABASE_PORT
default: '5432'
containers: [telapi]

- name: DATABASE_NAME
required: true
containers: [telapi]

- name: DATABASE_URL
required: true
sensitive: true
containers: [telapi]
compose:
template: 'postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}'
encode:
- DATABASE_PASSWORD
- DATABASE_USERNAME

compose.encode is the list of variable names to URL-encode before substitution (@, :, &, etc. in passwords and usernames are common). Seed tooling does not create an SSM / SM entry for composed variables — they exist only at runtime, after fetch-env.sh runs.

Validation

fetch-env.sh enforces:

  • Every required: true variable resolved to a non-empty value, or
  • Every required_when: VAR=value whose condition is satisfied resolved to a non-empty value.

Failure prints:

FATAL: Required variables missing from SSM/SM/bootstrap:
- DATABASE_URL
- REDIS_PASSWORD (required when REDIS_TLS_ENABLED=true)

…and exits non-zero, which aborts the calling init.sh before any container starts.

Conventions across the platform

  • Secret bundle naming — use ${NAMESPACE}/<service>/secrets for the per-service secret with all sensitive keys. Multiple secrets are supported but harder to audit.
  • source: local is for instance / infrastructure factsENVIRONMENT, HOSTNAME, NAMESPACE, ECR_REGISTRY, ECR_TAG, AWS_REGION, AWS keys (when not using instance profile), BASTION_PUBLIC_KEY, the optional DB_MASTER_SECRET_ARN. Everything else lives in SSM / SM.
  • Defaults are for sane fallbacks, not real values — production LOG_LEVEL belongs in SSM, not as a default:. Defaults exist so a developer running the same compose file locally can omit unimportant variables.
  • containers: is documentation, not enforcement — the actual variable injection comes from ${VAR} substitution in docker-compose.yaml. Keep containers: accurate so show-env.sh and seed tooling line up.

See also