Tutorial 4: Dynamic Configs¶
So far, your webmon configs have been static: you list your sites, set your intervals, and you're done. But what happens when you need to monitor 20 sites, each with slightly different check intervals, custom headers, or per-site notification rules?
You could copy-paste 20 blocks. Or you could let the config generate itself.
Dracon has a set of composition instructions (!define, !if, !each, !fn) that run at load time and produce the final config. Think of them as a lightweight templating layer that lives inside YAML.
Time: ~15 minutes.
Variables with !define¶
The simplest instruction. !define creates a variable that you can use in ${...} interpolations anywhere below it:
# config/base.yaml
!define environment: dev
database:
host: "db.${environment}.internal"
port: 5432
name: "webmon_${environment}"
After composition, the !define line disappears and the interpolations resolve:
The variable environment is available to everything in the same scope (the mapping where it was defined, and anything nested inside it).
Soft defaults with !set_default¶
!define always sets the variable, overwriting any previous value. Sometimes you want a fallback that only applies when the caller hasn't provided one. That's !set_default:
# config/base.yaml
!set_default environment: dev
!set_default log_level: INFO
database:
host: "db.${environment}.internal"
log_level: ${log_level}
If someone loads this file and passes ++environment=prod from the CLI (or defines environment in an outer file), the !set_default is skipped. If they don't, it falls back to dev.
The rule is simple:
!definealways wins. It sets the variable unconditionally.!set_defaultonly sets it if nobody else has.
Use !set_default in base/template files. Use !define in the files that make the final call.
Contracts with !require¶
When you write a config fragment meant to be included by other files, you sometimes need the caller to provide certain variables. !require declares that contract:
# config/notifications/email-template.yaml
!require notify_email: "Email address for alerts (e.g. ops@example.com)"
!require environment: "Deployment environment"
email:
to: ${notify_email}
subject: "[webmon] [${environment}] Site down"
from: "webmon-${environment}@example.com"
If this file is included without notify_email or environment being defined somewhere, composition fails with a clear error:
The hint message is just for humans reading the error. Make it useful.
Conditionals with !if¶
You want SSL settings in prod but not in dev. !if handles that.
Shorthand form¶
The short form includes a block only when the condition is truthy:
!define environment: prod
database:
host: "db.${environment}.internal"
port: 5432
!if ${environment == 'prod'}:
ssl:
cert: /etc/ssl/webmon.pem
key: /etc/ssl/webmon.key
log_level: WARN
When environment is prod, the ssl and log_level keys are added to the mapping. When it's anything else, they're left out entirely.
Then/else form¶
For choosing between two options:
!define environment: prod
database:
!if ${environment == 'prod'}:
then:
host: db.prod.internal
password: ${getenv('PROD_DB_PASSWORD')}
else:
host: localhost
password: dev-pass
port: 5432
The then branch is used when the condition is truthy, else when it's falsy. The then/else wrapper keys are removed; their contents get spliced into the parent.
Iteration with !each¶
This is the one that saves you from copy-pasting. !each repeats a block for every item in a list.
Generating a list¶
Say you want to create a monitoring config entry for each site:
!define sites:
- example.com
- status.example.com
- api.example.com
checks:
!each(site) ${sites}:
- url: "https://${site}"
interval: 30
timeout: 10
After composition:
checks:
- url: https://example.com
interval: 30
timeout: 10
- url: https://status.example.com
interval: 30
timeout: 10
- url: https://api.example.com
interval: 30
timeout: 10
The !each(site) tag declares the loop variable. The key expression ${sites} is what gets iterated over. The value (the - url: ... block) is the template that gets duplicated for each item.
Generating a map¶
You can also produce mapping entries with dynamic keys. This requires the keys to be interpolated so they're unique:
!define regions:
- us-east
- eu-west
- ap-south
endpoints:
!each(region) ${regions}:
${region}: "https://${region}.monitor.example.com"
Result:
endpoints:
us-east: https://us-east.monitor.example.com
eu-west: https://eu-west.monitor.example.com
ap-south: https://ap-south.monitor.example.com
You can also iterate over more structured data. If your items are dicts, just access their fields:
!define sites:
- { name: example.com, interval: 30 }
- { name: api.example.com, interval: 10 }
- { name: status.example.com, interval: 60 }
checks:
!each(site) ${sites}:
- url: "https://${site['name']}"
interval: ${site['interval']}
timeout: 10
Inline functions with !fn¶
When your template block is more than a few lines, or you want to reuse it in multiple places, extract it into a function with !fn:
!define make_check: !fn
!require site_name: "domain to monitor"
!set_default interval: 30
!set_default timeout: 10
url: "https://${site_name}"
interval: ${interval}
timeout: ${timeout}
health_endpoint: "https://${site_name}/health"
This defines make_check as a callable template. The !require and !set_default lines declare its parameters: site_name is required, interval and timeout have defaults.
Calling with a tag¶
You call it by using the function name as a YAML tag:
checks:
primary: !make_check { site_name: example.com }
api: !make_check { site_name: api.example.com, interval: 10 }
status: !make_check
site_name: status.example.com
interval: 60
timeout: 30
Both the flow syntax ({ key: value }) and block syntax work. The result of each call is the template body with the arguments substituted in:
checks:
primary:
url: https://example.com
interval: 30
timeout: 10
health_endpoint: https://example.com/health
api:
url: https://api.example.com
interval: 10
timeout: 10
health_endpoint: https://api.example.com/health
status:
url: https://status.example.com
interval: 60
timeout: 30
health_endpoint: https://status.example.com/health
Calling from expressions¶
You can also call !fn templates inside ${...} interpolations:
This is handy when you need the result as part of a larger expression.
If the result of an expression call is going to become a tag, the cleaner move is usually to alias it first:
!define CheckBuilder: ${pick_check_builder(kind=kind)}
check: !CheckBuilder
site_name: api.example.com
That keeps the selection logic in the expression and the constructed object in ordinary YAML.
Combining everything¶
Here's a real-world-ish example that uses !define, !each, !if, and !fn together. The goal: generate monitoring configs for multiple sites, with SSL checks only in prod.
# config/monitoring.yaml
!set_default environment: dev
!define sites:
- { name: example.com, interval: 30, critical: true }
- { name: api.example.com, interval: 10, critical: true }
- { name: docs.example.com, interval: 120, critical: false }
!define make_check: !fn
!require site: "site config dict"
url: "https://${site['name']}"
interval: ${site['interval']}
timeout: 10
!if ${site['critical']}:
notify: ops@example.com
priority: high
!if ${environment == 'prod'}:
ssl_verify: true
ssl_expiry_warn_days: 30
checks:
!each(site) ${sites}:
${site['name']}: !make_check { site: "${site}" }
Load it and check the result:
Output:
checks:
example.com:
url: https://example.com
interval: 30
timeout: 10
notify: ops@example.com
priority: high
ssl_verify: true
ssl_expiry_warn_days: 30
api.example.com:
url: https://api.example.com
interval: 10
timeout: 10
notify: ops@example.com
priority: high
ssl_verify: true
ssl_expiry_warn_days: 30
docs.example.com:
url: https://docs.example.com
interval: 120
timeout: 10
ssl_verify: true
ssl_expiry_warn_days: 30
Notice that docs.example.com doesn't have notify or priority (because critical is false), but it still has the SSL settings (because we're in prod). Switch to ++environment=dev and all the SSL lines vanish.
That's 50 lines of config generating a fully-typed monitoring setup for any number of sites, with environment-aware behavior.
What you've learned¶
!definesets a variable unconditionally;!set_defaultsets it only if not already provided!requiredeclares that a variable must be provided, with a hint message for the error!ifconditionally includes blocks, with a shorthand form (include-or-not) and athen/elseform!each(var) ${list}:iterates over a list, duplicating its body for each item. Works for both list and map generation.!fndefines a reusable template with parameters (!requirefor required,!set_defaultfor optional). Call it as a tag (!name { args }) or from an expression (${name(args)})- These instructions compose naturally:
!eachcan call!fn,!fnbodies can contain!if, and so on