Environments & Variable Files
env_groups and environments inline or as external files - one workflow, many targets, selected with --env
Environment separation in OrchStep is declarative: shared values go in
groups, per-environment values in environment definitions, and you pick
the target at the CLI with --env. Steps stay environment-agnostic.
This chapter covers both styles - inline blocks and externalized
variable files - plus the naming patterns that keep them maintainable.
Style 1: inline blocks
name: env-demo
defaults:
replicas: "1"
db_host: "localhost"
env_groups:
nonprod:
vars:
replicas: "2"
log_level: "debug"
prod:
vars:
replicas: "10"
log_level: "error"
environments:
dev:
group: nonprod
vars:
db_host: "dev-db"
staging:
group: nonprod
vars:
db_host: "staging-db"
replicas: "4" # env overrides its group
production:
group: prod
vars:
db_host: "prod-db"
tasks:
show:
steps:
- name: print
func: shell
do: 'echo "replicas={{ vars.replicas }} db={{ vars.db_host }}"'orchstep run show --env dev # replicas=2 db=dev-db
orchstep run show --env staging # replicas=4 db=staging-db
orchstep run show --env production # replicas=10 db=prod-db
orchstep run show --env production --var replicas=99 # CLI still winsInline is right for a handful of environments. When the blocks grow - or when different people own different environments - externalize them.
Style 2: external environment files (env_config)
Point the workflow at a directory; each environment becomes a file. The
filename IS the hierarchy: running --env nonprod-dev-instance34 loads
defaults.yml, then nonprod.yml, then nonprod-dev.yml, then
nonprod-dev-instance34.yml - later overrides earlier. Each file is a map of
values (scalars or whole objects), with no group:/vars: nesting. Click
through the layout:
environments/is the default -env_configis optional. If your directory is namedenvironments/and sits next to the workflow, you can omit theenv_config:block entirely - OrchStep auto-detects it. Declareenv_config: { env_dir: ... }only to point at a differently-named directory. An explicitenv_config:always wins, and an inlineenvironments:block takes precedence over the convention.
name: env-demo
env_config:
env_dir: environmentsResolution with those exact files (later overrides earlier):
orchstep run show # replicas=1 db=localhost (defaults.yml)
orchstep run show --env nonprod # replicas=2 log=debug
orchstep run show --env nonprod-dev # replicas=2 db=dev-db
orchstep run show --env nonprod-dev-instance34 # replicas=1 db=dev-db-34 (instance wins)
orchstep run show --env prod-production # replicas=10 db=prod-dbTip:
--envselects the target and--varalways wins over it, so a developer can pin a single value for one run -orchstep run show --env production --var replicas=1- without editing any file.
Naming patterns that scale
defaults.yml # org-wide baseline
<group>.yml # nonprod.yml, prod.yml
<group>-<env>.yml # nonprod-dev.yml, prod-production.yml
<group>-<env>-<instance>.yml # nonprod-staging-db645.ymlGuidelines that hold up in practice:
- Two groups beat five.
nonprod/prodcovers most estates; resist a group per environment (that is just environments again). - Instances are for sharded things - a database replica, a region, a
tenant:
prod-production-eu1.yml. - Keep secrets out. These files belong in git; inject secrets at
runtime via
--var,--vars-filepointing outside the repo, or OS env ({{ env.SECRET }}).
Style 3: ad-hoc variable files (--vars-file)
Independent of environments, any flat YAML file can be loaded at run time:
# overrides/load-test.yml
replicas: "50"
log_level: "warn"orchstep run deploy --env staging --vars-file overrides/load-test.ymlConvention: keep them in an overrides/ directory named by purpose
(load-test.yml, dr-failover.yml, local-dev.yml) - they answer
"what is different about THIS run", while environments/ answers "what
is different about this TARGET".
The full precedence ladder
defaults < group < environment (<instance) < task vars < step vars < --vars-file < --varVerify before you run: orchstep validate
A group is a shared layer, not always a complete target. With Style 2 it is
easy to leave a variable unset for one environment - a run then renders it as an
empty <no value>. orchstep validate catches this statically: it scans every
vars.X a task references and reports which would be unresolved under each
environment, as a coverage matrix.
Take a deploy task that references version and target, with the env files
from the naming pattern above:
# orchstep.yml
tasks:
deploy:
steps:
- { name: ship, func: shell, do: 'echo "deploy {{ vars.version }} to {{ vars.target }}"' }environments/
defaults.yml version: "2.4.0" # every environment inherits this
nonprod.yml # group layer — sets shared vars, but NOT target
nonprod-dev.yml target: dev-cluster # concrete environment
prod.yml # group layer — NOT target
prod-production.yml target: prod-cluster # concrete environmentversion lives in defaults.yml (so it resolves everywhere); target is only
in the concrete *-dev / *-production files. validate shows exactly that:
$ orchstep validate deploy
coverage matrix (✓ supplied · – absent):
variable (none) nonprod nonprod-dev prod prod-production
target – – ✓ – ✓
version ✓ ✓ ✓ ✓ ✓
findings:
⚠ nonprod : 1 unresolved (target)Here target is supplied by nonprod-dev/prod-production (concrete
environments) but not by the nonprod/prod group layers - exactly what you
want to know before selecting one. If a group file supplies everything (a single
shared config applied to many instances), it shows complete and is a valid
target. Use --env <name> for a per-step report, and --strict to fail CI when
a referenced variable is supplied by no environment. See
orchstep validate.
See it in a real pipeline
Environment Promotion deploys a service through dev/staging/production with the inline style plus approval gates and health checks.
Or clone a runnable example: serve-multi-envs
shows the external env_config: style (one file per environment under
environments/), and serve-demo
shows the inline style - both in github.com/orchstep/orchstep-demos.