---
title: 'Workspaces and Environments'
source: 'https://academia.sh/en/courses/infrastructure-as-code/workspaces-and-environments'
course: 'Infrastructure as Code'
language: en
updated: '2026-09-13T05:08:25+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Builds three environments in workspace and separate-directory form'
    - 'Counts environment-specific differences and duplicated fields'
    - 'Ties the number of signals that say which environment you are in to wrong applies'
    - 'Measures how many environments change together when the definition changes, and the divergence'
---

# Workspaces and Environments

Two setups for three environments: a single definition plus workspace versus separate directories, the count of environment-specific differences, the number of signals that say which environment you are in and applying to the wrong one, and how many environments change together when the definition changes, along with the divergence.

The previous lesson split state into parts, and environment appeared in the part names. How the
environment distinction itself is built was never examined. If the definition of the three
environments is largely the same, there are two paths: write a single definition and pick the
environment with a setting, or keep a separate directory for each environment. This lesson
actually builds both and measures the difference between them with three numbers.

How similar the environments are to each other — **environment parity** — was measured earlier
and is not repeated here. The question here is not similarity, it is **where that similarity is
written**.

## Three Instances of the Same Definition

**SM19.** The configuration language below is the small form defined for this lesson; the
`environment(...)` expression picks a value according to the selected environment. The script
writes both setups to disk: a single definition plus workspace, and three separate directories.

```js
// setup.mjs — the three environments are built two ways: single definition + workspace, and separate directories.
// The configuration language is the small form defined for this lesson; the environment(...) expression picks a value per environment.
import { writeFileSync, mkdirSync, rmSync } from "node:fs";

const environments = ["test", "staging", "production"];
const DEFINITION = `resource subnet backbone {
  block = environment(test="10.0.0.0/16", staging="10.1.0.0/16", production="10.2.0.0/16")
}
resource machine collector {
  count      = environment(test=1, staging=2, production=6)
  cores      = environment(test=2, staging=2, production=8)
  memory_gb  = environment(test=8, staging=8, production=32)
  image      = "measurement-collector-4.2"
}
resource queue read {
  retention_hours = environment(test=6, staging=24, production=168)
  partitions      = environment(test=1, staging=4, production=12)
}
resource data_store measurement {
  size_gb = environment(test=20, staging=100, production=2000)
  backup  = environment(test=false, staging=false, production=true)
}`;

// Resolves the environment(...) expression for the given environment; a non-conditional line is left as is.
const resolve = (text, env) => text.replace(/environment\(([^)]*)\)/g, (_, ic) => {
  const match = new RegExp(`${env}=([^,]+)`).exec(ic);
  return match[1].trim();
});

rmSync("workspace", { recursive: true, force: true });
rmSync("separate-dir", { recursive: true, force: true });
mkdirSync("workspace/state", { recursive: true });
writeFileSync("workspace/definition.txt", DEFINITION);
writeFileSync("workspace/.selected", "test");
for (const e of environments)
  writeFileSync(`workspace/state/${e}.json`, JSON.stringify({ env: e, items: [] }));
for (const e of environments) {
  mkdirSync(`separate-dir/${e}`, { recursive: true });
  writeFileSync(`separate-dir/${e}/definition.txt`, resolve(DEFINITION, e));
  writeFileSync(`separate-dir/${e}/state.json`, JSON.stringify({ env: e, items: [] }));
}

const lines = DEFINITION.split("\n");
const conditional = lines.filter((s) => s.includes("environment(")).length;
const fields = lines.filter((s) => s.includes("=")).length;
const separateLines = environments.reduce((t, e) => t + resolve(DEFINITION, e).split("\n").length, 0);
const sharedFields = fields - conditional;

console.log(`workspace: 1 definition file, ${lines.length} lines, ` +
  `${fields} fields (${conditional} environment-specific, ${sharedFields} shared), 3 state files`);
console.log(`separate dir: 3 definition files, ${separateLines} lines, ` +
  `${fields * 3} fields (${conditional * 3} written separately, ${sharedFields * 3} duplicated), 3 state files`);
console.log(`environment-specific difference: ${conditional} fields; identical across all three: ${sharedFields} fields`);
```

```
workspace: 1 definition file, 17 lines, 9 fields (8 environment-specific, 1 shared), 3 state files
separate dir: 3 definition files, 51 lines, 27 fields (24 written separately, 3 duplicated), 3 state files
environment-specific difference: 8 fields; identical across all three: 1 fields
```

Both setups describe the same infrastructure, keep the same three state files, and produce the
same environment distinction. The difference is in the amount of text written, and in **where the
difference stands**.

In the workspace form, the eight environment-specific differences sit in a single file, inside
eight conditional expressions. In the separate-directory form, the same eight differences spread
across twenty-four separate lines — each environment carries its own value in its own file — and
the one field identical across all three environments is duplicated three times.

What this first pair of numbers says is this: collecting the difference in one place makes it
**visible**. To learn where the three environments diverge, looking at one file is enough in the
workspace form; in the separate-directory form, three files have to be compared. Against that, in
the separate-directory form each environment's definition can be read on its own: understanding
what production is does not require resolving conditionals in your head.

On the state side, there is no difference at all between the two setups: both keep one state file
per environment, three in total. The numbers from the previous two lessons therefore apply the
same way to both — the lock is taken per environment, the three environments can apply without
waiting on each other, and losing one environment's state only makes that environment
unmanageable. The workspace shares the **definition**; the one thing it does not share is state,
and this distinction comes back at the end of the lesson.

## Which Environment Am I In

The second measurement belongs to operations. Before pressing the button, how many signals can the
person starting the apply read to know which environment they are in?

**SM20.** In the workspace form, the selected environment sits in a hidden file and is changed
with a separate command; it leaves no trace on the command line. In the separate-directory form,
the choice is the directory the command runs in, and it shows up in the path. **SM21.** The
probability of skipping the selection step is tied to the number of signals: fifteen percent
without a signal, three percent with one. **SM22.** A step that prints the environment name to the
screen and asks for confirmation before the apply catches nine out of ten skipped selections. The
generator is written inside the lesson, and the seed is visible.

```js
// env-choice.mjs — the step of knowing which environment you are in, and a model of applying to the wrong one.
import { readFileSync, writeFileSync } from "node:fs";

let x = 20260801 >>> 0;                          // seed is visible
const rand = () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
const environments = ["test", "staging", "production"];

// How many signals can the person starting the apply read to know which environment they are in?
console.log("which environment am I in: workspace -> 0 signals on screen, 1 extra command (selected file)");
console.log("                           separate dir -> 1 signal in the path, 0 extra commands");

const run = (label, forgetProb, confirmCatch) => {
  writeFileSync("workspace/.selected", "test");
  let wrong = 0, fellToProd = 0;
  for (let i = 0; i < 200; i++) {
    const target = environments[Math.floor(rand() * 3)];
    const current = readFileSync("workspace/.selected", "utf8");
    let picked = target;
    if (rand() < forgetProb) {                   // selection step skipped: previous environment stands
      picked = current;
      if (rand() < confirmCatch) picked = target; // confirmation step caught it
    }
    writeFileSync("workspace/.selected", picked);
    if (picked !== target) { wrong++; if (picked === "production") fellToProd++; }
  }
  console.log(`${label.padEnd(29)}: 200 applies, ${String(wrong).padStart(2)} wrong environment, ` +
    `${fellToProd} fell to production`);
};

run("workspace", 0.15, 0);
run("separate dir", 0.03, 0);
run("workspace + mandatory confirm", 0.15, 0.9);
```

```
which environment am I in: workspace -> 0 signals on screen, 1 extra command (selected file)
                           separate dir -> 1 signal in the path, 0 extra commands
workspace                    : 200 applies, 23 wrong environment, 8 fell to production
separate dir                 : 200 applies,  4 wrong environment, 1 fell to production
workspace + mandatory confirm: 200 applies,  2 wrong environment, 1 fell to production
```

Out of two hundred applies, the workspace form went to the wrong environment twenty-three times,
eight of which fell to production. In the separate-directory form, the count drops to four, and
the fall to production drops to one. The source of the difference is one single thing: whether
there is a signal on screen saying which environment this is.

The third row matters more. When a confirmation step that prints the environment name before the
apply was added to the workspace form, wrong applies dropped from twenty-three to two — better
than the separate-directory form. The result says that neither setup is inherently safe: what is
safe is the environment being **visible**. Separate directories give this for free because the
path is already on screen; in the workspace form, the same visibility has to be built separately.

The cost of applying to the wrong environment ties back to the measurement in this course's first
lesson. Each of the eight applies that fell to production writes test values into production's
state: machine count drops from six to one, the data store from two thousand gigabytes to twenty.
The gap between record and reality here is produced not by drift but by the record itself — and
the moment the apply finishes, both the record and reality are wrong.

## How Many Environments Change When the Definition Changes

The third measurement is about time. When a field is added to the definition, how many
environments are forced to change together? **SM23.** Eight of twelve changes concern all three
environments, four are environment-specific; in the separate-directory form, each manual move is
skipped with a one-in-four probability. **SM24.** The apply was only run in the test environment.

```js
// change.mjs — how many environments are forced to change together when the definition changes, and the divergence.
import { readFileSync, writeFileSync, appendFileSync } from "node:fs";

let x = 991 >>> 0;                               // seed is visible
const rand = () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
const environments = ["test", "staging", "production"];

// Twelve changes: eight concern all three environments, four are environment-specific.
const changes = [];
for (let i = 1; i <= 8; i++) changes.push({ no: i, scope: "all" });
for (let i = 9; i <= 12; i++) changes.push({ no: i, scope: environments[(i - 9) % 3] });

let newConditional = 0, moveNeeded = 0, moveSkipped = 0;
for (const d of changes) {
  if (d.scope === "all") {                       // single definition: one line, all three environments
    appendFileSync("workspace/definition.txt", `\n  new_field_${d.no} = "d${d.no}"`);
    for (const e of environments) {               // separate dir: moved by hand into each directory
      moveNeeded++;
      if (rand() < 0.25) { moveSkipped++; continue; }
      appendFileSync(`separate-dir/${e}/definition.txt`, `\n  new_field_${d.no} = "d${d.no}"`);
    }
  } else {                                        // environment-specific: needs a conditional expression
    newConditional++;
    const inner = environments.map((e) => `${e}=${e === d.scope ? `"d${d.no}"` : '""'}`).join(", ");
    appendFileSync("workspace/definition.txt", `\n  new_field_${d.no} = environment(${inner})`);
    appendFileSync(`separate-dir/${d.scope}/definition.txt`, `\n  new_field_${d.no} = "d${d.no}"`);
  }
}

const conditional = (s) => s.split("\n").filter((l) => l.includes("environment(")).length;
const fields = (file) => new Set(readFileSync(file, "utf8").split("\n")
  .filter((l) => l.includes("new_field_")).map((l) => /new_field_\d+/.exec(l)[0]));
const expected = (e) => changes.filter((d) => d.scope === "all" || d.scope === e).length;
const missing = environments.map((e) => expected(e) - fields(`separate-dir/${e}/definition.txt`).size);

console.log(`workspace: 1 definition file, ${changes.length} changes processed`);
console.log(`  conditional expressions 8 -> ${conditional(readFileSync("workspace/definition.txt", "utf8"))} ` +
  `(each environment-specific change adds one conditional)`);
console.log(`  shared change: forced to change together 3/3 environments, diverged fields: 0`);
console.log(`separate dir: 3 definition files, ${changes.length} changes processed`);
console.log(`  shared change required ${moveNeeded} moves, ${moveSkipped} moves skipped`);
console.log(`  shared change: forced to change together 1/3 environments, remaining 2 by hand`);
console.log(`  diverged fields: ${missing.reduce((a, b) => a + b, 0)} ` +
  `(${environments.map((e, i) => `${e} ${missing[i]}`).join(", ")})`);

// The definition changed, the apply only ran in the test environment.
const behind = (form) => environments.filter((e) => e !== "test")
  .map((e) => `${e} ${expected(e) - (form === "separate" ? missing[environments.indexOf(e)] : 0)}`).join(", ");
console.log(`record vs. reality (only test applied), fields waiting to be applied:`);
console.log(`  workspace -> ${behind("workspace")} | separate dir -> ${behind("separate")}`);
```

```
workspace: 1 definition file, 12 changes processed
  conditional expressions 8 -> 12 (each environment-specific change adds one conditional)
  shared change: forced to change together 3/3 environments, diverged fields: 0
separate dir: 3 definition files, 12 changes processed
  shared change required 24 moves, 2 moves skipped
  shared change: forced to change together 1/3 environments, remaining 2 by hand
  diverged fields: 2 (test 0, staging 2, production 0)
record vs. reality (only test applied), fields waiting to be applied:
  workspace -> staging 9, production 9 | separate dir -> staging 7, production 9
```

In the workspace form, a shared change is a single line and changes the definition of all three
environments **forced together**: 3/3. Diverged fields are zero, because there is no second file
to diverge. The cost is paid at the other end: four environment-specific changes gave birth to
four new conditional expressions; the conditional count rose from eight to twelve, and the file
became a little less readable with every environment difference.

In the separate-directory form, the same eight shared changes required twenty-four manual moves,
and two were skipped. The result is a two-field divergence in the staging environment. No one
reports this divergence; both definitions are valid on their own, and both applies run without
error. The difference is visible only when the three files are compared.

The skip rate itself is a measurement too. A one-in-four probability produced a two-field
divergence from twelve changes in one cycle; if the rate stays the same, the gap grows a little
bigger every cycle, and because no apply reports it, it accumulates. That the divergence lands in
the staging environment in this run is an accident of the model — which environment will diverge
is not known in advance, only that one will. This also means the separate-directory form needs a
step that **checks** the moves: without a check that compares the three definitions and reports
differences that are not environment-specific, divergence only surfaces as a difference in
behavior in production.

The last two numbers show the **record's own risk**. The definition changed in both forms, the
apply only ran in the test environment. In the workspace form, staging and production are each
left nine fields behind; in the separate-directory form, staging is seven, production is nine.
The record has changed for all three environments, reality for only one. A single definition does
not prevent the difference from **accumulating**; it only ensures it is written in one place. The
gap between environments is born not in the definition, but in **apply lag**.

## The Trade-off

The three measurements point in three different directions, and there is no single winner. When
environment-specific difference is small, the workspace form wins: one file, zero divergence,
eight conditionals. As environment-specific difference grows, the conditional expression count
swallows the definition, and separate directories stay readable. The risk of applying to the wrong
environment is five times higher in the workspace form, but a confirmation step brings it down to
two — meaning this difference is not the setup's doing, it is the doing of the guard placed around
the setup.

The one thing worth dwelling on is what stays the same in both forms: the state of the three
environments is three separate files. The workspace shares only the single definition; it does not
share state. So a change applied in one environment does not happen automatically in the others,
and the record being the same for all three environments does not mean reality is the same.

## Summary

- The same infrastructure was built two ways: a single definition file, seventeen lines and
  eight conditional expressions; three separate directories, fifty-one lines, twenty-four fields
  written separately and three duplicated.
- Environment-specific difference is eight fields, one field stays identical across all three
  environments; the difference is collected in one file in the workspace form, and spread across
  three files in the separate-directory form.
- Out of two hundred applies, the count of going to the wrong environment is twenty-three in the
  workspace form, four in the separate-directory form; a confirmation step printing the
  environment name brought the count down to two.
- A shared change in the workspace form changes all three environments together and produces
  zero divergence; in the separate-directory form, two of twenty-four manual moves were skipped
  and two fields diverged.
- Four environment-specific changes raised the conditional count from eight to twelve in the
  workspace form; in the separate-directory form, each stayed a single line in a single file.
- The definition changed for three environments, the apply ran in one: in the workspace form two
  environments were left nine fields behind, and in the separate-directory form staging seven and
  production nine. The gap between environments is born not in the definition but in
  apply lag.

## Next Step

Both setups in this lesson tried to solve one repetition: the same infrastructure being told
three times. The workspace turned the repetition into a conditional expression; the separate
directory repeated it outright. Neither eliminated the repetition, they only moved where it
lives. When ten teams, instead of three environments, want to set up the same queue, the same
collector machine, the same archive, neither form is enough; what is needed is packaging the
definition itself, and naming and versioning its inputs. The remaining lessons in this topic take
up the infrastructure module: input surface, version pinning, and how a breaking change
propagates to every dependent user.
