skip to content

How would you lay out an infrastructure-as-code repository so the same infrastructure can be deployed to dev, staging and production?

level: middleimportance: must knowfreq 78%

answer

  1. same code, different inputs
  2. components once, wiring per environment
  3. values file or directory per environment
  4. separate state and credentials per environment
  5. promote a version, never fork the code

basics

~20 s

Define each piece of infrastructure once as a reusable component and vary only the inputs per environment. Give every environment its own thin top-level configuration, its own values file and its own isolated state and credentials, so an apply in dev cannot touch production.

solid answer

~40 s

The invariant I care about is *same definitions, different inputs*. The shared components — network, cluster, database, service — are written once with an explicit input interface. Each environment then gets a thin top-level configuration that wires those components together and supplies its own values, usually from a per-environment values file checked into the repo. Two common shapes: one top-level configuration selected at run time with a values file per environment, or a directory per environment each holding its own small top-level configuration. The directory-per-environment shape is more verbose but lets prod legitimately differ (an extra replica, a stricter deletion guard) without conditionals leaking into shared code. Whichever shape, the non-negotiables are the same: separate recorded state per environment, separate credentials, and environment differences expressed as *values*, not as forked copies of the code.

go deeper

for a junior

Be ready to say that the same infrastructure code should build every environment and that only the input values differ. Naming a concrete layout — a values file per environment, or a folder per environment — is enough at this level.

for a middle

Explain the two mainstream layouts and their tradeoff: one shared composition with per-environment values is DRY but forces structural sameness; a folder per environment repeats a little wiring in exchange for letting prod legitimately differ.

for a senior

Show that you treat isolation as the real control: separate recorded state, separate credentials, ideally separate accounts, so a dev mistake physically cannot reach production. Explain how you keep environments from drifting apart over months.

for a principal

Own the estate-wide argument: how the layout scales to dozens of services and teams, what it costs to change layout later, and how you keep non-production representative enough to be worth its bill without duplicating production spend.

## The property you are actually protecting The reason this question is asked in nearly every infrastructure interview is that it has one right answer at the level of principle and several defensible answers at the level of layout. The principle: **the same code must produce every environment, and environments must differ only by input values.** If staging and production are two hand-edited copies of the same directory, they start identical and diverge by a small amount every month, and by the time you need staging to rehearse a production change it no longer resembles production. A change you validated in one environment must be the *same change* you then apply in the next. ## Layer 1: reusable components Underneath any layout is a set of components — network, data store, cluster, service — each defined once with a declared input interface (region, size, replica count, retention, tags) and a declared set of outputs other pieces consume. These are environment-agnostic on purpose. They should not contain the string `prod` anywhere. The moment a component knows which environment it is in, you have lost the ability to test it anywhere except the environment it was written for. ## Layer 2: the environment layout Three shapes are common. **One top-level configuration plus a values file per environment.** The repository holds a single root that composes the components, and a values file for each environment supplying sizes, counts, CIDRs and names. You pick the file at run time. ``` components/network/ components/database/ root/ <- one composition values/dev.vars values/staging.vars values/prod.vars ``` Maximally DRY, minimal duplication, and the diff between two environments is literally the diff between two small files — which is a genuinely useful property in a review. The cost: every environment must be *structurally* identical, because there is only one composition. The first time production needs a resource that dev does not have, teams start adding conditionals to the shared root, and it decays. **A directory per environment, each with its own thin root.** Each environment owns a small composition file that calls the shared components with its own values. ``` components/... environments/dev/ environments/staging/ environments/prod/ ``` Slightly repetitive, and that is the point: the repetition is confined to a handful of wiring lines, while the substance still lives in the shared components. Production may add a component the others lack, or set a stricter deletion guard, without a conditional anywhere. The risk is neglect — if nobody diffs the environment roots against each other, they drift structurally. Many teams add a review habit or a lint check that compares them. **One configuration with a switchable named environment.** Some tools offer a built-in per-environment selector over a single configuration, keeping a separate recorded state per name. It is the fastest to set up and the easiest to misfire: one working directory, one code path, and often one set of credentials, so "apply to the wrong environment" is a single mistyped selector away. It suits short-lived or throwaway environments far better than it suits production. ## Isolation is not optional Whatever the layout, these must be per-environment: **the recorded state** (so a destroy plan computed against dev can never enumerate production resources), **the credentials** (so the dev pipeline holds no production authority), and ideally **the account or subscription boundary** itself. A shared credential defeats every layout choice above; the directory structure is documentation, but the credential is the actual control. ## Non-production is never identical A good answer admits the tension. Production is bigger and more expensive; a faithful copy would cost as much. So you keep the *shape* identical — same components, same wiring, same topology — and let scalar values differ: fewer replicas, smaller instance classes, shorter retention, no multi-region replica. Where the shape must differ, prefer expressing it as "this environment composes one extra component" rather than "the shared component behaves differently in prod". ## Promotion falls out of the layout When the layout is right, promoting a change is boring: bump the version of a shared component in the dev root, apply, exercise it, then make the identical bump in staging, then in prod. Each step is a reviewed change with its own preview. When the layout is wrong — three hand-maintained copies — promotion is re-implementation, and every re-implementation is a chance to differ. ## Answering in an interview State the invariant first, then pick a layout and defend it, then name the isolation requirements. Interviewers are listening for whether you understand that the folder structure is a means to an end, and that the end is: what ran in staging is what runs in prod.

  • What if production genuinely needs a resource that dev does not have at all?
    Express it as composition, not as a conditional inside a shared component: production's top-level configuration includes one extra component. That keeps the shared code environment-agnostic and testable everywhere. If the difference is only scale — replica count, instance size, retention — that is an input value, not a structural difference, and it should stay a value.
  • Why is a single shared state record across all three environments a problem, even if the resources are named distinctly?
    Because blast radius follows the state, not the naming convention. Any apply computes a diff over the whole record, so a bad change or a mistaken destroy can enumerate production resources while you thought you were touching dev. It also serialises everyone behind one lock and makes the production resource inventory readable by anyone who can run a dev plan.
  • How do you keep non-production cheap without making it useless as a rehearsal?
    Keep the topology identical and shrink the scalars: fewer replicas, smaller instance classes, shorter retention, single region. Preserve the things a rehearsal actually tests — the same component versions, the same dependency wiring, the same identity and network boundaries. Cutting a whole component out of staging is what makes it useless, not running it small.

saying these in an interview costs you the question

  • Copy the production directory and hand-edit the values to create staging.
  • One shared state record for all environments is fine if you are careful.
  • Environments must be byte-identical, including instance sizes and counts.
  • Hardcode the environment name inside the shared component definitions.
  • Same working directory and same credentials, just point it at prod.

context