Skip to content
academia.sh

Lesson 27 / 27

Pipeline Integration

An infrastructure change is run through a pipeline: the plan stage and the apply stage are separated, and approval stands in between. The plan the approver approves without reading, the reality that goes stale between plan and apply, the breadth of the pipeline's apply permission, and a manual change being silently reverted, colliding with, or never seen by the pipeline's next run are all counted.

Published Updated

In this lesson

  • Measures the approval that sits between plan and apply: how many plans are approved without being read, how many minutes review takes
  • Counts the reality that goes stale between plan and apply and the number of items the freshness gate stops
  • Narrows the pipeline's apply permission by operation count and counts the changes left on the table
  • Counts a manual change being silently reverted, colliding, and never being seen at the pipeline's next run separately
Contents

The previous lesson baked the image, the one before that applied the setting. Neither asked one thing: who triggers these, and who approves them. All of this course’s measurements were run by hand, and in a hand-run flow a person stands between plan and apply and sees the two back to back. A pipeline separates the two: the plan is produced at one stage, stored, submitted for approval; the apply runs at another moment. M22/K04 measured what a gate caught and what it missed; that measure is not repeated, it is applied.

IV37. The pipeline runs the regional measurement network’s — the fictional software collecting water-meter readings’ — infrastructure record in two stages, and passes 18 changes over 30 days. IV38. An item holds 3 lines in the plan, 12 lines are read per minute, and the approver does not read a plan past 40 lines. IV39. The seeds 20260901–20260903 are visible and the numbers are an order of magnitude. IV40. The provider offers 9 resource types and 4 operations, the environment has 84 fictional resources, and the narrow permission is derived from the last 12 runs. IV41. 26 manual changes occur over 30 days; the touched item’s probability of being in the record’s scope is 0.62, an in-scope item’s probability of being protected is 0.24, and the urgent-fix rate is 0.35. IV42. The freshness gate regenerates the plan at apply time and compares it against the approved one, adding 0.5 minutes per change.

Plan, Approval, Apply

Approval is a signature; what can be measured is how much reading stands behind it, and how much time passes between the signature and the apply — a plan is a snapshot of a moment, and it goes stale.

// measurement-network/pipeline.mjs — plan stage, approval, apply stage (model; a kind, not a product)
export const generator = (seed) => () =>
  (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;

// plan lines per item; lines read per minute; the line count where the approver stops reading;
// minutes the freshness gate adds per change.
const LINE = 3, SPEED = 12, THRESHOLD = 40, REGEN = 0.5;

// 18 infrastructure changes pass through the pipeline over 30 days; quantities come from the generator.
export const changes = (seed = 20260901) => {
  const r = generator(seed);
  return Array.from({ length: 18 }, () => {
    const item = 1 + Math.floor(r() * 14);
    return { item, line: item * LINE + 6, wait: Math.round(r() * 470) / 10,
      changed: r() < 0.3, drifted: 1 + Math.floor(r() * 3) };
  });
};

// gate=true: at apply time the plan is regenerated and compared against the approved one.
export const run = (list, gate) => {
  const unread = list.filter((d) => d.line > THRESHOLD);
  const stale = list.filter((d) => d.changed);
  const drifted = stale.reduce((t, d) => t + d.drifted, 0);
  return {
    unread: unread.length, unreadItems: unread.reduce((t, d) => t + d.item, 0),
    minutes: list.reduce((t, d) =>
      t + (d.line <= THRESHOLD ? d.line / SPEED : 0.5) + (gate ? REGEN : 0), 0),
    stale: stale.length, unprotected: gate ? 0 : drifted, drifted,
    held: gate ? stale.length : 0, applied: 18 - (gate ? stale.length : 0),
  };
};

if (import.meta.url === `file://${process.argv[1]}`) {
  const list = changes(), sum = (a) => list.reduce((t, d) => t + a(d), 0);
  console.log(`18 changes, ${sum((d) => d.item)} items, ${sum((d) => d.line)} plan lines; ` +
    `average ${(sum((d) => d.wait) / 18).toFixed(1)} hours between plan and apply\n`);
  console.log("freshness gate".padEnd(16) + "approved".padEnd(11) + "unread".padEnd(11) +
    "unread items".padEnd(17) + "review time".padEnd(16) + "stale plan".padEnd(12) +
    "unprotected items".padEnd(19) + "applied");
  for (const gate of [false, true]) {
    const s = run(list, gate);
    console.log((gate ? "yes" : "no").padEnd(16) + "18/18".padEnd(11) +
      `${s.unread}/18`.padEnd(11) + `${s.unreadItems}/${sum((d) => d.item)}`.padEnd(17) +
      `${s.minutes.toFixed(1)} min`.padEnd(16) + `${s.stale}/18`.padEnd(12) +
      `${s.unprotected}/${s.drifted}`.padEnd(19) + `${s.applied}/18`);
  }
  const noGate = run(list, false);
  console.log(`\nthe approver does not read once the plan exceeds ${THRESHOLD} lines: ${noGate.unread} changes, ` +
    `${noGate.unreadItems} items approved without review`);
}
18 changes, 129 items, 495 plan lines; average 28.6 hours between plan and apply

freshness gate  approved   unread     unread items     review time     stale plan  unprotected items  applied
no              18/18      4/18       49/129           29.0 min        3/18        5/5                18/18
yes             18/18      4/18       49/129           38.0 min        3/18        0/5                15/18

the approver does not read once the plan exceeds 40 lines: 4 changes, 49 items approved without review

Eighteen of eighteen changes passed approval; the approval rate says nothing. What says something is the second column: 4 plans were approved without being read, and those four plans carried 49 of 129 items — thirty-eight percent of the items were applied unlooked-at. This is not an attention problem, it is a scale problem; the review load grows linearly, as already counted in declarative-provisioning/07.

The second measure is the time between plan and apply: an average of 28.6 hours, and reality does not stand still over that time. 3 of the eighteen plans were stale when applied, and 5 items that did not appear in the approved plan entered the apply; the approved document and the applied work were not the same thing.

One of this course’s gates only does its job here. Format checking, policy scanning, infrastructure testing, and cost estimation can all run outside a pipeline too; the freshness gate cannot. Regenerating the plan at apply time and comparing it against the approved plan requires the approved plan to be a stored document; a hand-run flow has nothing stored to compare against. The gate adds 9 minutes, holds 3 applies, and rescues 5 items.

The Pipeline’s Permission

The pipeline now runs the apply itself, and for that it has an identity on the infrastructure. If the permission is broad, the pipeline can do anything; if that identity falls, anything becomes possible.

// measurement-network/permission.mjs — the pipeline's apply permission over the infrastructure (model)
import { generator, changes } from "./pipeline.mjs";

const TYPE = ["machine", "disk", "network", "subnet", "security rule", "queue", "data store",
  "object store", "identity"];
const OPERATION = ["create", "read", "update", "delete"];
const RESOURCE = [14, 22, 3, 6, 19, 2, 4, 3, 11];   // fictional resources per type; 84 total
const WINDOW = 12;   // past runs the permission is derived from

// Each change touches a few (type, operation) pairs; the pair set comes from the generator.
const touches = (seed = 20260902) => {
  const r = generator(seed);
  return changes().map(() => {
    const c = new Set(), n = 1 + Math.floor(r() * 3);
    for (let i = 0; i < n; i++)
      c.add(`${TYPE[Math.floor(r() * 9)]}/${OPERATION[Math.floor(r() * 4)]}`);
    return [...c];
  });
};

const touch = touches();
const all = TYPE.flatMap((t) => OPERATION.map((i) => `${t}/${i}`));
const narrow = [...new Set(touch.slice(0, WINDOW).flat())];
const noDelete = narrow.filter((c) => !c.endsWith("/delete"));
const deletable = (y) => TYPE.reduce((t, x, i) => t + (y.includes(`${x}/delete`) ? RESOURCE[i] : 0), 0);

console.log(`${TYPE.length} resource types x ${OPERATION.length} operations = ${all.length} operations; ` +
  `${RESOURCE.reduce((a, b) => a + b)} fictional resources\n`);
console.log("permission".padEnd(23) + "operations".padEnd(12) + "passed".padEnd(14) +
  "left over".padEnd(12) + "deletable resources");
for (const [ad, y] of [["broad (all)", all], [`narrow (from ${WINDOW} runs)`, narrow],
  ["narrow, no delete", noDelete]]) {
  const passed = touch.filter((c) => c.every((x) => y.includes(x))).length;
  console.log(ad.padEnd(23) + `${y.length}/36`.padEnd(12) + `${passed}/18`.padEnd(14) +
    `${18 - passed}/18`.padEnd(12) + `${deletable(y)}/84`);
}
const newPairs = [...new Set(touch.slice(WINDOW).flat())].filter((c) => !narrow.includes(c));
console.log(`\nnarrow permission was derived by looking at the past; the next 6 runs produced ${newPairs.length} ` +
  `new operation pairs: ${newPairs.join(", ")}`);
9 resource types x 4 operations = 36 operations; 84 fictional resources

permission             operations  passed        left over   deletable resources
broad (all)            36/36       18/18         0/18        84/84
narrow (from 12 runs)  19/36       14/18         4/18        14/84
narrow, no delete      18/36       13/18         5/18        0/84

narrow permission was derived by looking at the past; the next 6 runs produced 5 new operation pairs: subnet/delete, queue/update, security rule/delete, subnet/create, disk/create

Broad permission finishes the table in one line: 36 of 36 operations, 18 of 18 changes, zero left on the table. The last column is the cost — if the pipeline’s identity is compromised, 84 of 84 resources can be deleted; the pipeline is a wider access than any record written in this course grants.

When permission is narrowed by looking at the past twelve runs, the operation count drops to 19, deletable resources to 14; the cost is that 4 of the 18 changes are left on the table. Once delete permission is removed too, deletable resources drop to zero, and what is left on the table rises to 5. The bottom line writes down why narrowing is not permanent: the next six runs produced 5 new operation pairs never seen before. Permission derived from the past leaves the future’s work on the table, and for what is left, the manual path counted in the first lesson carries on exactly as before.

When a Manual Change Hits the Pipeline

Machines still get touched by hand after a pipeline is set up; the question is what happens to that touch at the pipeline’s next run.

// measurement-network/collision.mjs — what a manual change becomes at the pipeline's next run (model)
import { generator } from "./pipeline.mjs";

const RUN = [1.4, 3.1, 4.0, 6.8, 8.2, 9.9, 12.5, 14.1, 15.0, 17.7,
  19.2, 21.4, 22.8, 24.3, 25.1, 27.6, 28.9, 29.5];   // day of each of the 18 pipeline runs (fictional)
const COVERAGE = 0.62;    // probability that a manually touched item is in the record's scope
const PROTECTION = 0.24;  // probability that an in-scope item is protected or recreates on change

// 26 manual changes occur over 30 days; each touches a single item.
const manual = (seed = 20260903) => {
  const r = generator(seed);
  return Array.from({ length: 26 }, () => {
    const day = Math.round(r() * 300) / 10;
    const inScope = r() < COVERAGE;
    return { day, inScope, protected: inScope && r() < PROTECTION, urgent: r() < 0.35 };
  });
};

const RESULT = { "silently reverted": [], "conflicting (pipeline halts)": [], "unnoticed": [] };
for (const o of manual()) {
  const next = RUN.find((g) => g > o.day);
  const outcome = !o.inScope ? "unnoticed"
    : o.protected ? "conflicting (pipeline halts)" : "silently reverted";
  // An out-of-record item is never seen on any run; it has no delay either.
  const delay = !o.inScope || next === undefined ? null : next - o.day;
  RESULT[outcome].push({ ...o, delay });
}

console.log("26 manual changes, 30 days, 18 pipeline runs\n");
console.log("outcome".padEnd(31) + "count".padEnd(9) + "urgent".padEnd(8) + "delay");
for (const [ad, list] of Object.entries(RESULT)) {
  const g = list.map((o) => o.delay).filter((v) => v !== null);
  console.log(ad.padEnd(31) + `${list.length}/26`.padEnd(9) +
    String(list.filter((o) => o.urgent).length).padEnd(8) +
    (g.length ? `${(g.reduce((a, b) => a + b, 0) / g.length).toFixed(1)} days` : "-"));
}
const reverted = RESULT["silently reverted"];
console.log(`\n${reverted.length} reverted changes produced no error, all of them were a single line in the plan; ` +
  `${reverted.filter((o) => o.urgent).length} of them were urgent fixes`);
26 manual changes, 30 days, 18 pipeline runs

outcome                        count    urgent  delay
silently reverted              15/26    4       1.2 days
conflicting (pipeline halts)   3/26     1       0.7 days
unnoticed                      8/26     2       -

15 reverted changes produced no error, all of them were a single line in the plan; 4 of them were urgent fixes

The three rows are three separate events, and only the middle one is visible. In 3 collisions the pipeline halts and a person makes the call; these count as proof the pipeline is working. 15 changes are silently reverted and vanish after an average of 1.2 days; none of them produce an error, because the pipeline was built to do exactly that. 4 of those fifteen were urgent fixes — because their records were never updated, they went back to the old value the record wanted. iac-fundamentals/05 already counted a rollback breaking a live fix; the pipeline makes this reversion faster and quieter.

Every reversion was a single line in the plan; given that four of the plans were approved without being read, the source of the silence is not the pipeline, it is the unread plan line.

The third row writes down the course’s rule one last time: 8 items are items the pipeline never sees at all. Because they are out of record, they neither revert nor collide; they remain in place while the pipeline runs flawlessly.

Summary

  • Eighteen of eighteen changes passed approval, but 4 plans were approved without being read, and those four plans carried 49 of 129 items (38 percent); the approval rate is not a measure.
  • An average of 28.6 hours passed between plan and apply, 3 of the 18 plans were stale, and 5 items not visible at approval time entered the apply.
  • The freshness gate is meaningful only in a pipeline, because it requires the approved plan to be stored; it adds 9 minutes, holds 3 applies, and rescues 5 items.
  • Under broad permission, if the identity falls, 84 of 84 resources can be deleted; once permission narrows to 19 operations, deletable resources drop to 14, work left on the table rises to 4 changes, and the next six runs produce 5 new operation pairs.
  • Of 26 manual changes, 15 were silently reverted (4 urgent fixes), 3 collided, and 8 were never seen at all because they were out of record.

Course Wrap-Up

Lesson What enters the record Gap between record and reality The record’s cost or limit
iac-fundamentals/01 8-line description 9 items diverge 5 items out of scope
iac-fundamentals/02 9-item target 6/9 against 2/9 5 guard lines
iac-fundamentals/03 image, 96 events 10/12 machines unique 3 fixes lost
iac-fundamentals/04 34 items, 35 definitions 6 items conflict 5 items in no record
iac-fundamentals/05 26 items 12 drifted, 6 close 12 items out of scope
declarative-provisioning/01 3 blocks, 14 assignments 8 defects at apply 9 fields blind to the type
declarative-provisioning/02 20 fields 36 fields on target 16 fields not in record
declarative-provisioning/03 8 resources, 5 data sources scan 8/13 items 2 external fields invisible
declarative-provisioning/04 14 variables, 24 values 4 wrong inputs passed 48 hand-written values across 6 environments
declarative-provisioning/05 14 resources, 23 edges requirements 27, seen 23 4 requirements not in record
declarative-provisioning/06 3 forms, 10 resources 1 versus 6 items 525 machine-seconds downtime
declarative-provisioning/07 plan, 11 items 8 of 13 addresses hold 39.6 minutes across 30 regions
declarative-provisioning/08 20 of 24 fields first plan, 4 items 4 fields out of record
state-and-modules/01 56 fields, 80 in state 4 of 33 items out of record 3 items do not return on loss
state-and-modules/02 two concurrent applies 12 items lost without a lock 12 resources out of record
state-and-modules/03 620 items, 673 edges 66 edges cross the boundary 3 items out of record
state-and-modules/04 17 or 51 lines 23 versus 4 wrong environments 2 environments, 9 fields behind each
state-and-modules/05 module, 20 lines 0 of 4 items seen 1 resource out of record
state-and-modules/06 9 definitions, 12 versions 9/9 do not say what is running 25 versions behind
state-and-modules/07 7 trace points 14 copies, marking → 4 3 points stay open
iac-validation/01 11 resources, 59 lines three checks 16/16 4 unseen rejections
iac-validation/02 62 lines, 7 policies 3 missed, 2 false alarms 3 computed values invisible
iac-validation/03 15 defects three levels 12/15 11 leftovers, 0 in record
iac-validation/04 24 items, 12 changes 9 at 0.51x–1.45x 11 items not estimable
iac-validation/05 1700 record lines 1598 vs. 1530 hold 27/61 settings out of scope
iac-validation/06 content record 32 entries 52 entries in images 20 entries out of record
iac-validation/07 18 changes, 495 lines 3 stale plans, 5 items 8 items unseen

This is the course’s rule: infrastructure can only be managed as well as its record; every item the record does not cover is a source of drift. Three limits repeat in the last column. In nineteen rows, the limit is an item the record does not cover — an out-of-record resource, a provider default, an unseen requirement, a computed value, an indirect component. In five rows the limit is the record’s own operating overhead: a guard line, review minutes, a version left behind, a delayed apply. In three rows the limit is an unrecoverable loss: a deleted fix, a machine out of service, an item that does not return. Growing the record shrinks the first group and grows the second; this trade-off was never zeroed out in any lesson.

Infrastructure is now tied to a record: it is written, planned, checked, priced, approved, and becomes work a pipeline applies. But all of this course’s measurements belonged to the moment of change. No one yet sees what the infrastructure does while it runs: which machine carries how much load, which queue is backing up. M22/K06 Observability and Operations starts with that question — who decides what gets measured?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close