Automation
Environments Should Differ by Config, Not Code
The moment dev, staging, and prod start diverging in your code instead of your data, you've lost the guarantee that what you tested is what you shipped — keep one code path and let declared configuration carry every environment difference.
- Automation
- Infrastructure
- Configuration
- Ansible
When I build automation that has to run across several environments — a dev box, a staging tier, production — I hold one rule above almost everything else: the environments differ in data, never in code. Same roles, same tasks, same logic everywhere; the only thing that changes between dev and prod is the values fed in. The instant an environment difference leaks into the code path itself, you’ve quietly broken the most valuable property automation gives you — that the thing you validated in dev is the same thing that runs in prod.
The fork starts innocent and metastasizes
Nobody decides to fork their environments. It starts as one little exception: a conditional for “just prod,” a separate task file because staging is weird, a branch that special-cases the one host that’s different. Each is reasonable in isolation. Collectively they’re a slow-motion disaster, because now dev and prod execute different instructions, and the confidence you got from a clean dev run no longer transfers. You didn’t test prod’s path; you tested a sibling of it.
If dev and prod run different code, then “it worked in dev” is a statement about dev, and nothing else.
The cruelest part is that the divergence hides. The code looks like it handles all environments because all the branches are right there in the file. But only one branch ran where you were watching, and the others are untested promises.
One code path, many declared configurations
The shape I aim for instead: environment-agnostic logic, with every environment-specific value declared as data outside the code. The role doesn’t know it’s running in prod; it knows it received a set of variables, and those variables happen to describe prod. Dev and prod aren’t different programs — they’re the same program with different inputs. Swap the inputs and the same code produces the right result for each place.
Concretely, that looks like:
- Logic that takes parameters, never logic that asks “which environment am I?” and branches on the answer.
- Per-environment values in their own files — the hostnames, sizes, endpoints, toggles — kept entirely separate from the tasks that consume them.
- Sensible defaults plus explicit overrides, so a new environment is “declare its values,” not “add a code branch for it.”
The test for whether you’ve done it right: could you stand up a brand-new environment by writing only a config file, touching no logic? If yes, your environments differ by configuration. If you’d have to edit code, they don’t yet.
Configuration is data, and data is safer to vary
There’s a reason pushing the variation into config is better than pushing it into code, beyond tidiness. Data is inert — a wrong value in prod’s config is a bad value, contained and easy to diff against dev’s. A wrong branch in prod’s code is untested behavior executing for the first time in the worst place. Varying data is a bounded risk; varying code is an unbounded one. So you concentrate the difference where it’s cheapest to get wrong and easiest to review: a values file you can read top to bottom and compare across environments at a glance.
This isn’t an argument that config is harmless — a config language is still something you have to treat with care. It’s an argument that if something must differ per environment, config is the safer place to hold the difference than a fork in the logic.
When an environment genuinely needs different behavior
Sometimes prod really does need to do something dev doesn’t — an extra
safety check, a step that only makes sense at scale. Even then, the move isn’t a
hardcoded “if prod” branch; it’s a flag. Express the behavior as a capability
the configuration turns on or off, defaulted sanely, so the code path is still
uniform and the difference is still declared data. “Run the extra verification
when strict_checks is true” keeps one code path and lets prod’s config opt in.
“If environment == prod” scatters the decision into the logic and starts the fork
all over again.
The payoff is transferable confidence
Keep environments differing by config and you get the thing you actually wanted: a dev run that means something about prod, a new environment that’s a data entry not a code change, and a diff between two environments that’s a readable list of values instead of an archaeology dig through conditionals. It’s the same discipline behind keeping a map of your environments and remembering that config management isn’t a scheduler — put the variation where it belongs and let the logic stay boringly identical everywhere. If you’ve pulled an environment fork back into clean config, tell me how it went.