---
title: 'What Is Infrastructure as Code'
source: 'https://academia.sh/en/courses/infrastructure-as-code/what-is-infrastructure-as-code'
course: 'Infrastructure as Code'
language: en
updated: '2026-09-13T05:08:24+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts the difference between two setups of the same environment item by item and separates where the difference comes from'
    - 'Measures change traceability with a single criterion: how many times the who, when, what-changed question gets answered'
    - 'Counts the item the record does not cover, and the cost of tying to a record in lines and extra steps'
---

# What Is Infrastructure as Code

The same staging environment is set up twice — once by hand from a prose request, once from a record — and the two setups are compared item by item. For a quarter's worth of changes, how many times the question of who, when, and what changed gets answered, how many items fall outside the record's scope, and the cost of tying to a record in lines and extra steps are counted.

The previous course tied the application's path to production to the repository: when a branch
merges, the pipeline runs, gates are passed, and deployment proceeds on its own. The application's
path is written from end to end. The machines it stands on, the queue, the data store, and the
network, though, are set up through a different route — a request is opened, someone reads that
request and sets up the environment, and the result of the setup lives only in the environment
itself.

**Infrastructure as code** is tying infrastructure to a readable **record**: what the environment
should be is written in a file, and the environment is set up from that file. This course's unit
of measure is the record, and every lesson counts three things — the difference between what the
record says and reality, the record's scope, and the record's cost. This lesson sets up the first:
the same environment is set up twice, once by hand from a prose request and once from a record,
then compared **item by item**. Then, for a quarter's worth of changes, it measures how many times
the question "who, when, what changed" gets answered, how many items fall outside the record's
scope, and the cost of tying to a record, in lines and extra steps.

The measurement runs on the **regional measurement network**: **fictional** software that collects
water-meter readings, verifies them, converts them into invoices, and opens field work orders. It
has three environments; only the staging environment is set up here. Infrastructure is not really
provisioned — resources and setups are modeled with `node`; the record and request files that feed
the model are genuinely written to disk.

**IF1.** The staging environment is represented by six resources — one network, two machine
groups, one queue, one data store, one object store. An item is one attribute of a resource; the
record writes twenty items.
**IF2.** The setup request is prose, and seven of its eleven items give no number. **A hand is a
model**; its interpretation is written in advance — there is no randomness.
**IF3.** The provider default is five items: settings the record does not write, that arise in the
environment regardless. They are born the same in both setups.
**IF4.** A quarter sees fourteen changes; each change asks three questions — who, when, what
changed. Each change's route is written in advance.
**IF5.** A change routed through the record answers all three questions; a change that falls into
a work ticket answers two (it does not record what changed); an untracked change answers none.
**IF6.** The record route has three extra steps per change: write the record, get it reviewed,
read the plan.

## Same Environment, Two Recipes

```bash
#!/usr/bin/env bash
# Two recipes for the same environment: the record (configuration language) and a prose setup request.
set -e

cat > record.infra <<'SON'
# Our configuration language's format: resource <type> "<name>" { <key> = <value> }
# A value is a number, a "string", or true/false; a line starting with # is a comment.
resource network "measurement-network" { block = "10.40.0.0/16"  private = true  region = "north" }
resource machine "collector" { count = 3  size = "medium"  disk_gb = 40  image = "base-7" }
resource machine "verifier" { count = 2  size = "small"  disk_gb = 20  image = "base-7" }
resource queue "reading-queue" { retention_hours = 72  partitions = 6  dead_letter = true }
resource data_store "measurement-store" { size_gb = 200  backup_days = 14  version = "5.2" }
resource object_store "archive" { class = "sparse"  versioning = true  lifecycle_days = 365 }
SON

cat > request.txt <<'SON'
staging environment setup request
1. open the measurement network as private, address block 10.40.0.0/16, north region
2. open enough collector machines, size medium, image base-7
3. size the collector disk to the measurement volume
4. two verifier machines, size small, disk 20 GB, image base-7
5. open the reading queue, set retention close to production, dead letter on
6. set the queue partition count to the worker count
7. open the measurement store at 200 GB, version 5.2
8. set the store's backup window to the corporate standard
9. open an object store for the archive, versioning on
10. choose the archive's storage class by cost
11. write a reasonable value for the archive lifecycle day count
SON
```

The configuration language is defined here with its own small format: a `resource` line carries a
type, a name, and key-value pairs inside curly braces. Six lines count twenty items, and each item
is tied to a single value — two readers understand the same thing. The request targets the same
environment, but seven of its items give a criterion instead of a number: "enough", "to the
measurement volume", "close to production", "the corporate standard", "by cost", "a reasonable
value". These seven points are the request's **unwritten** part, and whoever builds it fills them
in.

```js
// setup.mjs — parses the record and builds the same environment two ways: from the record and by hand. Both are models.
import { readFileSync } from "node:fs";

const BLOCK = /^resource\s+(\w+)\s+"([\w-]+)"\s*\{(.*)\}$/;      // the language's one-line format
export const readRecord = (text) => {
  const k = new Map();
  for (const line of text.split("\n")) {
    const m = BLOCK.exec(line.trim());                        // comments and blank lines are skipped
    if (m) for (const x of m[3].matchAll(/(\w+)\s*=\s*("[^"]*"|[\w./-]+)/g))
      k.set(`${m[1]}.${m[2]}.${x[1]}`, x[2].replaceAll('"', ""));
  }
  return k;
};
export const record = readRecord(readFileSync("record.infra", "utf8"));

// Provider default: items the record does not write, that arise in the environment regardless.
const providerDefaults = new Map([["network.measurement-network.egress_path", "open"],
  ["machine.collector.access_rule", "management-port-open"],
  ["queue.reading-queue.encryption", "provider-key"],
  ["data_store.measurement-store.maintenance_window", "sunday-03:00"],
  ["object_store.archive.access_rule", "in-account"]]);

// The hand's pre-written interpretation: the seven points the request gives no number for (2, 3, 5, 6, 8, 10, 11).
const handInterpretation = new Map([["machine.collector.count", "2"], ["machine.collector.disk_gb", "40"],
  ["queue.reading-queue.retention_hours", "24"], ["queue.reading-queue.partitions", "6"],
  ["data_store.measurement-store.backup_days", "7"], ["object_store.archive.class", "dense"],
  ["object_store.archive.lifecycle_days", "90"]]);
const handSkipped = ["object_store.archive.versioning"];        // written in the request, skipped in practice
const handAdded = new Map([["network.measurement-network.access_rule", "office-block"],
  ["machine.collector.extra_disk", "attached"], ["machine.verifier.dns_record", "verifier.internal"]]);

export const buildFromRecord = () => new Map([...record, ...providerDefaults]);
export const buildManually = () => {
  const o = new Map([...record, ...handInterpretation, ...providerDefaults, ...handAdded]);
  for (const a of handSkipped) o.delete(a);
  return o;
};
export const interpretationPoints = handInterpretation.size, outOfRecordCount = providerDefaults.size;
```

## The Difference, Item by Item

```js
// diff.mjs — compares the two setups item by item; fixes nothing.
import { record, buildFromRecord, buildManually, interpretationPoints, outOfRecordCount } from "./setup.mjs";

const a = buildFromRecord(), b = buildManually();
const all = [...new Set([...a.keys(), ...b.keys()])].sort();
const diff = all.filter((k) => a.get(k) !== b.get(k)).map((k) => ({ k,
  kind: !b.has(k) ? "missing" : !a.has(k) ? "extra" : "diverged" }));
const countBy = (t) => diff.filter((f) => f.kind === t).length;

console.log(`items  from record ${a.size}   by hand ${b.size}   union ${all.length}`);
console.log(`diff ${diff.length} items   (diverged ${countBy("diverged")}, missing ${countBy("missing")},` +
  ` extra ${countBy("extra")})`);
for (const f of diff)
  console.log(`  ${f.kind.padEnd(8)} ${f.k.padEnd(42)} ${(a.get(f.k) ?? "-").padEnd(20)}` +
    ` ${b.get(f.k) ?? "-"}`);
console.log(`items in the record ${record.size}   points the request gives no number for ${interpretationPoints}` +
  `   of these diverged ${diff.filter((f) => f.kind === "diverged").length}`);
console.log(`items outside the record's scope ${outOfRecordCount}   the same in both setups,` +
  ` written in neither`);
```

```
items  from record 25   by hand 27   union 28
diff 9 items   (diverged 5, missing 1, extra 3)
  diverged data_store.measurement-store.backup_days   14                   7
  diverged machine.collector.count                    3                    2
  extra    machine.collector.extra_disk               -                    attached
  extra    machine.verifier.dns_record                -                    verifier.internal
  extra    network.measurement-network.access_rule    -                    office-block
  diverged object_store.archive.class                 sparse               dense
  diverged object_store.archive.lifecycle_days        365                  90
  missing  object_store.archive.versioning            true                 -
  diverged queue.reading-queue.retention_hours        72                   24
items in the record 20   points the request gives no number for 7   of these diverged 5
items outside the record's scope 5   the same in both setups, written in neither
```

The two setups of the same environment diverged on nine items, and the difference comes from three
separate kinds. **Diverged** covers five items, five of the seven points the request gave no
number for: the remaining two are invisible because the hand's interpretation happened to land on
the same value as the record. These two are not a gain, they are a coincidence — the
interpretation-point count is still seven, only five of them diverged in this run. The one
**missing** item was written explicitly in the request and never applied; an item skipped in a
prose list leaves no trace anywhere. The three **extra** items never appear in the request at all:
an access rule, an extra disk, and a DNS record. They come from the routine of the person setting
up the environment and are written in no recipe — they exist once setup is done, but why they
exist lives only in one person's memory.

The direction of the difference is meaningful too. In four of the five diverged items, the hand's
value is **smaller** than the record's — two collectors instead of three, twenty-four hours instead
of seventy-two, seven days instead of fourteen, ninety days instead of three hundred sixty-five.
When a request without a criterion is read, the value chosen answers the load visible at that
moment; the value the record carries is the one decided once. The difference between the two
setups is therefore not a list of errors, it is a list of **two separate moments of decision**.

The last line is the second half of this lesson's measurement axis. The five items the record does
not cover are **the same in both setups**, because the provider gives rise to both: the egress
path, the management-port rule, the encryption key, the maintenance window, the in-account access
rule. Because the record says nothing about them, a comparison never shows them as a difference.
Writing the record does not zero out the difference; it makes the difference visible **only as far
as the record's scope reaches**.

## Who, When, What Changed

```js
// track.mjs — how traceable a quarter's changes are in the two environments. Model.
import { record } from "./setup.mjs";

// Fourteen changes in a quarter; written in advance, no randomness.
const CHANGES = [["machine.collector.count", "record"], ["machine.collector.size", "record"],
  ["queue.reading-queue.partitions", "record"], ["data_store.measurement-store.size_gb", "record"],
  ["machine.collector.access_rule", "hand"], ["object_store.archive.lifecycle_days", "record"],
  ["network.measurement-network.block", "record"], ["machine.verifier.count", "record"],
  ["queue.reading-queue.retention_hours", "hand"], ["data_store.measurement-store.backup_days", "record"],
  ["machine.verifier.image", "record"], ["object_store.archive.class", "record"],
  ["network.measurement-network.egress_path", "hand"], ["machine.collector.disk_gb", "record"]];
const ticketFiled = [0, 3, 7, 10];                    // the four that get a ticket opened in the manual environment

// A change routed through the record answers all three questions; a work ticket answers two (it does not record what changed).
const answered = { record: 3, ticket: 2, untracked: 0 };
const A = CHANGES.map(([, y]) => (y === "record" ? "record" : "untracked"));
const B = CHANGES.map((_, i) => (ticketFiled.includes(i) ? "ticket" : "untracked"));
const sumAnswers = (x) => x.reduce((t, s) => t + answered[s], 0);
const percent = (n) => (100 * n / (CHANGES.length * 3)).toFixed(1);

console.log(`changes ${CHANGES.length}   questions ${CHANGES.length * 3} (who / when / what changed)`);
for (const [name, x] of [["environment set up from the record", A], ["manually set up environment", B]])
  console.log(`  ${name.padEnd(35)}: answered ${sumAnswers(x)}/${CHANGES.length * 3}` +
    ` (${percent(sumAnswers(x))}%)   untracked changes ${x.filter((s) => s === "untracked").length}`);

const handOnly = CHANGES.filter(([, y]) => y === "hand").map(([k]) => k);
console.log(`${handOnly.length} changes that skip the record in the record-built environment:`);
for (const k of handOnly)
  console.log(`  ${k.padEnd(41)} ${record.has(k) ? "in the record  -> shows up as drift"
    : "not in the record  -> shows up in no comparison"}`);
```

```
changes 14   questions 42 (who / when / what changed)
  environment set up from the record : answered 33/42 (78.6%)   untracked changes 3
  manually set up environment        : answered 8/42 (19.0%)   untracked changes 10
3 changes that skip the record in the record-built environment:
  machine.collector.access_rule             not in the record  -> shows up in no comparison
  queue.reading-queue.retention_hours       in the record  -> shows up as drift
  network.measurement-network.egress_path   not in the record  -> shows up in no comparison
```

Thirty-three of the forty-two questions are answered on the record route, eight on the manual
route. The difference is not one of discipline, it is one of **where things are kept**: on the
record route a change lives in a file and the file is versioned, so all three questions are read
from the same place. On the work-ticket route two questions are answered — who opened it, when it
closed — but the third, **what changed**, is left to prose, and prose carries no measurable value.

The real information is in the last three lines. Even in the environment set up from the record,
three changes skipped the record, and the three produced three different outcomes. When an item
written in the record is changed by hand, a comparison shows it as **drift**; when an item not
written in the record is changed by hand, it shows up in no comparison at all. Traceability comes
not from the record's existence but from its **scope**: for an item the record never touches, there
is no difference at all between the record route and the manual route.

## The Record's Cost

```bash
#!/usr/bin/env bash
# The cost of tying to a record: lines written and extra steps per change.
printf 'lines written   request %s   record %s   parser and applier %s (once)\n' \
  "$(wc -l < request.txt | tr -d ' ')" "$(wc -l < record.infra | tr -d ' ')" \
  "$(wc -l < setup.mjs | tr -d ' ')"
printf 'record has %s item lines, %s items  -> items per line %s\n' \
  "$(grep -c '^resource' record.infra)" "20" "$(node -e 'console.log((20/6).toFixed(2))')"
printf 'extra step per change   hand route 0   record route 3 (write, get reviewed, read the plan)\n'
printf 'over a quarter (14 changes)  hand route 0 extra steps   record route %s extra steps\n' "$((14 * 3))"
```

```
lines written   request 12   record 8   parser and applier 38 (once)
record has 6 item lines, 20 items  -> items per line 3.33
extra step per change   hand route 0   record route 3 (write, get reviewed, read the plan)
over a quarter (14 changes)  hand route 0 extra steps   record route 42 extra steps
```

The record itself is shorter than the request: eight lines, six of them carrying items, 3.33 items
per line. The cost is not there. The thirty-eight-line parser and applier is written once and
reused for every environment — that is a one-time cost. The cost paid continuously is in the last
line: on the record route, every change asks for three extra steps, and over a quarter that comes
to forty-two extra steps. On the manual route these steps are zero; a person walks into the
environment and changes the value.

What is bought for the forty-two extra steps is countable too: twenty-five more questions answered
and twenty items made comparable. But what is not bought is just as clear. Five items stayed
outside the record, and the forty-two extra steps do not touch them; three changes skipped the
record, and the extra steps did not stop them either. A record does not produce an obligation, it
produces **common ground for comparison** — who uses that ground is a separate question, and the
rest of this course largely measures it.

## Summary

- The two setups of the same environment diverged on 9 items: 5 diverged (5 of the 7 points the
  request gave no number for), 1 missing (written, skipped), 3 extra (written in no recipe).
- Two of the request's seven interpretation points did not show up because the hand's
  interpretation happened to land on the same value as the record; the interpretation-point count
  did not change, only five of them diverged in this run.
- The 5 items the record does not cover were born the same in both setups; a comparison never
  shows them as a difference.
- 33 of a quarter's 42 questions were answered on the record route, 8 on the manual route; the
  difference comes not from discipline but from where the change is kept.
- The record is 8 lines, the parser and applier 38 lines (once); the continuous cost is 3 extra
  steps per change, 42 extra steps over a quarter.

## Next Step

In this lesson, the record was a list of items: each line counted a resource's attributes and
wrote what the environment **should be**. This is not the only form a record can take. The same
environment can also be written as a list of steps — first open the network, then open the
machines, then attach the queue — and that list also lives in a file, is versioned, is reviewed.
Both forms are records, and both ask for the same three extra steps.

The difference does not show up on the first run. The next lesson writes the same target in both
forms and runs both through **partial failure**: where the step list stops when the third step
errors, what the desired-state definition does; how much work each produces when rerun; and how
many lines each requires when the target changes.
