BLOG/CONFIGURATION MANAGEMENT
THEME
CONFIGURATION MANAGEMENT

Externalize env config into files

When your inline environments block stops fitting on a screen, move each target into its own file. The filename becomes the hierarchy — defaults, group, group-env — and --env picks the path.

May 17, 2026 OrchStep Team 6 minROLE: DevOps EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/externalize-env-config
VIEW SOURCE

Inline environments are great until they aren't. A nonprod/prod split with three targets fits in one orchstep.yml and reads at a glance. But add a few instances, give each environment a dozen values, and let three different people own dev, staging, and prod — and that single block becomes a merge-conflict magnet nobody wants to touch.

The fix is not a different model, just a different home. Point the workflow at a directory and let each target be its own file. The values are identical to the inline version; they just live where the person who owns them can edit one small file without rebasing the whole workflow.

The filename is the hierarchy

This is the part that makes it click: the filename encodes the layering. Running --env nonprod-dev loads defaults.yml, then nonprod.yml, then nonprod-dev.yml — in that order, later files overriding earlier ones. You do not declare the hierarchy anywhere; you name it.

orchstep.yml
name: externalized
# env config lives in files under environments/. The filename IS the hierarchy.
env_config:
  env_dir: environments
tasks:
  deploy:
    steps:
      - name: ship
        func: shell
        do: 'echo "deploy {{ vars.version }} to {{ vars.target }} (mon={{ vars.monitoring }} replicas={{ vars.replicas }})"'

Each file is a flat map of values — no group: or vars: nesting like the inline style needs. The nesting is the filename. A nonprod-staging.yml is understood to layer on top of nonprod.yml, which layers on top of defaults.yml.

What resolves to what

orchstep run deploy                       # version=2.4.0  replicas=1  (defaults.yml only)
orchstep run deploy --env nonprod-dev     # version=2.4.0-dev  target=dev-cluster  mon=basic
orchstep run deploy --env nonprod-staging # replicas=4  target=staging-cluster  version=2.4.0-rc1
orchstep run deploy --env prod-production # replicas=10 target=prod-cluster  mon=full

Look at nonprod-staging: replicas is 4 because the staging file overrides the 2 from the nonprod group, which overrode the 1 in defaults. Three files, one resolved value, last-writer-wins. The prod-production run never reads anything under nonprod--env only walks the chain its name describes.

The naming patterns that hold up

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.yml

A few rules that keep this from sprawling:

  • Two groups beat five. nonprod/prod covers most estates. A group per environment is just inline environments wearing a directory.
  • Instances are for sharded things — a replica, a region, a tenant: prod-production-eu1.yml. They are the most specific file, so they win.
  • Keep credentials out. These files belong in git. Inject secrets at runtime — that is a separate namespace entirely.

You can skip the env_config: block when the directory is literally named environments/ and sits next to the workflow — OrchStep auto-detects it. Declare env_config: { env_dir: ... } only to point at a differently-named folder.

Inline or external — same model

Inline (Style 1)External (Style 2)
Lives inone orchstep.yml blockone file per target under environments/
Hierarchy expressed bygroup: / vars: nestingthe filename (<group>-<env>.yml)
Best whena handful of small environmentsmany targets, or split ownership
Selected with--env <name>--env <name>

They are the same precedence ladder — defaults < group < environment < --var — just stored differently. You can start inline and externalize later without touching a single step, because the steps only ever read {{ vars.X }}.

Catch the gaps before you run

Externalizing introduces one new failure mode: a value present in one environment's file but forgotten in another. A run then renders it as an empty <no value>. orchstep validate turns that into a static coverage matrix across every environment — worth a post of its own.

Where to go next

The full tree above is runnable in orchstep-demos. The steps only echo — drop in your real deploy and --env does the rest.

#ENVIRONMENTS#CONFIGURATION#YAML#DEVOPS
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — CONFIGURATION MANAGEMENT