Lesson 04 / 15
Launch Templates
Manually launched instances and template-launched ones are compared field by field at the end of a period, and drifted instances and separate configuration variants are counted; when the template is updated, instances left behind, the number of versions running at the same time, and instances a security patch has not reached are worked out; the template's own input surface and the fields that cannot be undone are named.
Published Updated
In this lesson
- Compares manual and template launch with the same measure: field drift, drifted instances, and separate configuration variants
- Computes the number of instances left behind, the version distribution, and the instances a fix has not reached when the template is updated
- Counts the template's input surface: required fields, defaulted fields, and overridable fields
- Separates the cost of fixing drift into in-place fixes and re-creation, and names the six fields that cannot be undone
Contents
The previous lesson counted 1,450 instances opening a month on interruptible capacity. In each of those 1,450 launches, the same family, the same size, the same image, the same subnet, and the same startup script are chosen. If the choice is made manually, every launch is a separate write; every separate write is a chance to drift from the definition.
A launch template collects these items into a single definition: when an instance is launched, fields are not given one by one, the template’s name is given. This lesson measures how much of the gap the template closes. The measurement is made in three places — drift at launch time, instances left behind when the template is updated, and the template’s own input surface. M22/K01 measured drift in the same way, as the number of machines diverging from the definition; the magnitude here is the same, but the source is not drift accumulated while running, it is the write made at launch time.
CC19. The template has 12 fields; fields and drift probabilities are fictional. CC20. Over the period, 140 instances of the regional measurement network — the fictional software that collects readings from water meters — are launched; the seed 20260915 is visible and the generator is self-written. CC21. The fleet is 60 instances, runs for 90 days, each instance’s daily replacement probability is 0.02, and the template is updated every 21 days; seed 20260921. CC22. A security patch arrives with the template’s second version. CC23. An in-place fix takes 4 minutes, re-creation takes 35 minutes, and 2 instances are handled at once. CC24. An override is a deliberate act and is modeled separately from accidental drift.
How Many Times to Write the Same Definition
In manual launches, every field is given separately every time; in a template, a field is given once and, at launch time, can only be overridden. When the two paths are run over the same period and counted field by field, where the difference lies becomes visible.
// measurement-network/template-fields.mjs — manual launch versus template launch drift (model) export const generator = (seed) => () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648; export const LAUNCHES = 140; // instances launched over the period (fictional) // [name, required?, overridable?, mutable after creation?, manual drift, override] export const FIELD = [ ["instance family and size", true, true, true, 0.09, 0.020], ["machine image", true, true, false, 0.11, 0.010], ["subnet", true, true, false, 0.06, 0.005], ["security group", true, true, true, 0.08, 0.010], ["key pair identity", true, true, true, 0.05, 0.005], ["root disk size", false, true, true, 0.12, 0.020], ["root disk encryption", false, true, false, 0.07, 0.005], ["startup script", false, true, false, 0.10, 0.015], ["capacity type", false, true, false, 0.04, 0.030], ["placement preference", false, false, false, 0.05, 0.000], ["tags", false, true, true, 0.18, 0.040], ["monitoring agent", false, true, true, 0.09, 0.005], ]; // Fields that do not change after creation: their drift cannot be fixed in place, the instance is re-created. export const IRREVERSIBLE = FIELD.map(([, , , mutable], j) => (mutable ? -1 : j)).filter((j) => j >= 0); export const period = (seed) => { const r = generator(seed), drifted = FIELD.map(() => [0, 0]), signature = [new Set(), new Set()]; const fix = [[0, 0], [0, 0]]; // [re-creation, in-place fix] for (let i = 0; i < LAUNCHES; i++) { const trace = [[], []]; FIELD.forEach(([, , overridable, , manual, override], j) => { if (r() < manual) { drifted[j][0]++; trace[0].push(j); } if (overridable && r() < override) { drifted[j][1]++; trace[1].push(j); } }); signature[0].add(trace[0].join(",")); signature[1].add(trace[1].join(",")); [0, 1].forEach((k) => { if (trace[k].some((j) => IRREVERSIBLE.includes(j))) fix[k][0]++; else if (trace[k].length) fix[k][1]++; }); } return { drifted, fix, variants: signature.map((s) => s.size) }; }; if (import.meta.url === `file://${process.argv[1]}`) { const d = period(20260915); console.log(`${LAUNCHES} instances launched; seed 20260915\n`); console.log("field".padEnd(26) + "required".padEnd(10) + "overridable".padEnd(18) + "mutable later".padEnd(18) + "manual drift".padEnd(14) + "template drift"); FIELD.forEach(([name, required, overridable, mutable], j) => console.log(name.padEnd(26) + (required ? "yes" : "no").padEnd(10) + (overridable ? "yes" : "no").padEnd(18) + (mutable ? "yes" : "no").padEnd(18) + `${d.drifted[j][0]}/${LAUNCHES}`.padEnd(14) + `${d.drifted[j][1]}/${LAUNCHES}`)); const t = [0, 1].map((k) => d.drifted.reduce((s, x) => s + x[k], 0)); console.log(`\ntotal field drift: manual ${t[0]}, template ${t[1]} (${(t[0] / t[1]).toFixed(1)}x)`); console.log(`separate configuration variants: manual ${d.variants[0]}, template ${d.variants[1]}`); }
140 instances launched; seed 20260915 field required overridable mutable later manual drift template drift instance family and size yes yes yes 8/140 5/140 machine image yes yes no 12/140 4/140 subnet yes yes no 10/140 0/140 security group yes yes yes 9/140 1/140 key pair identity yes yes yes 8/140 2/140 root disk size no yes yes 18/140 2/140 root disk encryption no yes no 6/140 1/140 startup script no yes no 12/140 2/140 capacity type no yes no 5/140 7/140 placement preference no no no 4/140 0/140 tags no yes yes 30/140 9/140 monitoring agent no yes yes 12/140 1/140 total field drift: manual 134, template 34 (3.9x) separate configuration variants: manual 38, template 13
Total drift drops from 134 to 34 — nearly fourfold. But the real number is in the last line: the 140 manually launched instances produce 38 separate configuration variants, the template-launched ones produce 13. Variant count tells how many different things the fleet is made of; it is what determines, when investigating a problem and asking “how is this machine different from the others,” how many places the answer must be searched.
The subnet and placement preference rows draw the boundary of how much the template can close.
Placement preference is the one field that cannot be overridden, and its drift on the template
path is zero — it is the only field fully preserved. Subnet comes out at zero too, even though
it is overridable, because no one changes it deliberately; protection like this is usually enough
but is not guaranteed.
The capacity type row shows the reverse and must be read carefully: drift on the template path
is higher than manual — 7 against 5. This is not a bug, it is a usage. The previous lesson’s
result requires choosing between persistent and interruptible by job type, and that choice is made
per instance. The template removes accidental drift; it does not remove deliberate overrides,
and it should not. The measure’s job is to separate the two, not to suppress both.
What Happens When the Template Is Updated
A template is a definition; the definition changing does not change running instances. Whatever version an instance was launched with, it carries that version for its entire life. That is why the template’s version and the fleet’s version are not the same thing.
// measurement-network/template-version.mjs — how many instances fall behind when the template is updated (model) import { generator } from "./template-fields.mjs"; const FLEET = 60, DAYS = 90, CHANGE = 0.02, INTERVAL = 21; // daily instance-replacement probability const PATCH = 2; // version the security patch arrives with export const run = (seed = 20260921) => { const r = generator(seed); const instances = Array.from({ length: FLEET }, () => ({ version: 0, born: 0 })); const versions = [{ no: 0, day: 0, launched: FLEET }]; for (let g = 1; g <= DAYS; g++) { if (g % INTERVAL === 0) versions.push({ no: versions.length, day: g, launched: 0 }); const cur = versions[versions.length - 1]; for (const o of instances) if (r() < CHANGE) { o.version = cur.no; o.born = g; cur.launched++; } } return { instances, versions }; }; if (import.meta.url === `file://${process.argv[1]}`) { const { instances, versions } = run(); console.log(`${FLEET} instances, ${DAYS} days, daily replacement probability ${CHANGE}, ` + `template updated every ${INTERVAL} days; seed 20260921\n`); console.log("version".padEnd(9) + "release day".padEnd(13) + "launched with this version".padEnd(29) + "running at period end".padEnd(23) + "age of the oldest instance"); for (const s of versions) { const k = instances.filter((o) => o.version === s.no); console.log(`v${s.no}`.padEnd(9) + `day ${s.day}`.padEnd(13) + String(s.launched).padEnd(29) + `${k.length}/${FLEET}`.padEnd(23) + (k.length ? `${DAYS - Math.min(...k.map((o) => o.born))} days` : "—")); } const stale = instances.filter((o) => o.version < PATCH).length; const latest = versions[versions.length - 1]; console.log(`\nlatest version v${latest.no} shipped on day ${latest.day}; ${DAYS - latest.day} days later ` + `${FLEET - instances.filter((o) => o.version === latest.no).length}/${FLEET} instances are still behind`); console.log(`security patch arrived with v${PATCH} (day ${versions[PATCH].day}); ` + `${DAYS - versions[PATCH].day} days later ${stale}/${FLEET} instances are still on the old value`); console.log(`versions running at the same time: ` + `${new Set(instances.map((o) => o.version)).size}/${versions.length}`); }
60 instances, 90 days, daily replacement probability 0.02, template updated every 21 days; seed 20260921 version release day launched with this version running at period end age of the oldest instance v0 day 0 78 14/60 90 days v1 day 21 30 8/60 69 days v2 day 42 29 15/60 46 days v3 day 63 21 14/60 26 days v4 day 84 9 9/60 5 days latest version v4 shipped on day 84; 6 days later 51/60 instances are still behind security patch arrived with v2 (day 42); 48 days later 22/60 instances are still on the old value versions running at the same time: 5/5
The template was versioned five times, and at the end of the period all five versions are running. Even though the entire fleet was launched from a single definition, five different moments of that definition are standing at the same time. This is the time dimension of the same magnitude as the previous section’s variant count: there, drift was born at launch time; here, the fleet diverges even though the template has not changed.
The oldest instance is 90 days old and carries the first version. In a fleet with a daily replacement probability of 0.02, the probability of an instance living 90 days is low but not zero; 14 of 60 instances complete the period without ever changing. A plan that looks at the average does not see these instances, because the average instance age is under 50 days.
The security row is the cost of this. The patch arrived with the second version on day 42; 48 days later, 22 of 60 instances are still on the old value. Updating the template is not publishing a fix, only making it apply to instances launched from that point on. The fix reaches the fleet only as instances get replaced, and that speed is determined not by the template but by the fleet’s renewal policy.
The Template’s Own Surface
The template reduces drift, but it is itself an input surface: twelve fields sit in one place, and every field in that place can be wrong. The surface’s size is measured not by the field count but by how many fields can be overridden and by how fixable the drift is.
// measurement-network/template-surface.mjs — the template's own input surface and the cost of fixing drift (model) import { FIELD, LAUNCHES, IRREVERSIBLE, period } from "./template-fields.mjs"; const RECREATE = 35, IN_PLACE = 4, WINDOW = 2; // minutes, minutes, instances handled at once const count = (f) => FIELD.filter(f).length; if (import.meta.url === `file://${process.argv[1]}`) { console.log(`the template's input surface: ${FIELD.length} fields — ${count((a) => a[1])} required, ` + `${count((a) => !a[1])} defaulted, ${count((a) => a[2])} overridable, ` + `${IRREVERSIBLE.length} do not change after creation`); console.log(`field that cannot be overridden: ` + FIELD.filter((a) => !a[2]).map((a) => a[0]).join(", ") + "\n"); const d = period(20260915); console.log("path".padEnd(11) + "drifted instances".padEnd(19) + "fixed in place".padEnd(17) + "re-created".padEnd(19) + "fix wall-clock hours"); ["manual", "template"].forEach((path, k) => { const [recreated, inPlace] = d.fix[k]; const minutes = Math.ceil(recreated / WINDOW) * RECREATE + Math.ceil(inPlace / WINDOW) * IN_PLACE; console.log(path.padEnd(11) + `${recreated + inPlace}/${LAUNCHES}`.padEnd(19) + String(inPlace).padEnd(17) + String(recreated).padEnd(19) + `${(minutes / 60).toFixed(1)} h`); }); const KEPT = ["the template's fields", "which fields are overridable", "default values", "version policy", "who can update the template"]; const HANDED_OFF = ["template storage", "how long version history is retained", "the launch call's validation rules"]; console.log(`\ndecision ledger: ${KEPT.length} items kept, ${HANDED_OFF.length} items with the provider`); console.log(` with the provider: ${HANDED_OFF.join(", ")}`); console.log(` ${IRREVERSIBLE.length} fields that cannot be undone: ` + IRREVERSIBLE.map((j) => FIELD[j][0]).join(", ")); }
the template's input surface: 12 fields — 5 required, 7 defaulted, 11 overridable, 6 do not change after creation field that cannot be overridden: placement preference path drifted instances fixed in place re-created fix wall-clock hours manual 92/140 47 45 15.0 h template 32/140 18 14 4.7 h decision ledger: 5 items kept, 3 items with the provider with the provider: template storage, how long version history is retained, the launch call's validation rules 6 fields that cannot be undone: machine image, subnet, root disk encryption, startup script, capacity type, placement preference
The first line gives the template’s boundary in a single number: 11 of 12 fields can be overridden at launch time. The template is not a gate, it is a set of defaults; drift’s back door is not closed, it is narrow. The one fully closed field is placement preference, and it is no coincidence that this field shows zero drift on the template path.
The second table splits the cost of fixing into two items. In manual launches, 92 of 140 instances drift; 47 of these are fixed in place, and 45 must be re-created, because the drift is in a field that does not change after creation. The total wall-clock cost is 15.0 hours. On the template path, drifted instances drop to 32, instances needing re-creation to 14, and wall-clock cost to 4.7 hours. What the template actually gains is not the drop in drift count, it is the drop in unfixable drift: from 45 to 14.
The six fields that cannot be undone are the source of this distinction, and three of them come from this course’s previous lessons: machine image, subnet, and root disk encryption are given when the instance is created, the startup script freezes for that instance, capacity type cannot turn an instance launched as persistent into interruptible, and placement preference determines where the instance is placed and is not moved afterward. A wrong value in any of these six fields demands not a fix but a replacement.
The surface also has an unwritten side. The template does not have how many instances to launch; the template says what an instance will be, not how many there will be. That number is still given externally at every launch, and it has been given 140 times over the period. In the same way, the template does not write when a launched instance will be terminated; that is why 14 instances never change in the 90-day run.
In the decision ledger, five of eight items are kept, three are with the provider. The three that stay with the provider — template storage, how long version history is retained, and which fields the launch call validates — turn the template itself into a dependency: the five kept items are meaningful only while those three keep working.
Summary
- The template brings total field drift down from 134 to 34, but the more meaningful number is variants: 140 instances produce 38 separate configurations when launched manually and 13 when launched from the template.
- The template removes accidental drift, not deliberate overrides: the
capacity typefield shows 7 drifts on the template path and 5 on the manual path, and this is not a bug, it is a decision made by job type. - When the template is updated, no running instance changes: five versions run at the same time, and 6 days after the latest version ships, 51 of 60 instances are still behind.
- The security patch arrives with the second version, yet 48 days later 22 instances are still on the old value; the fix’s speed is determined not by the template but by the fleet’s renewal policy.
- The template’s input surface is 12 fields, and 11 can be overridden at launch time; the one field fully preserved is placement preference.
- The template’s real gain is in unfixable drift: instances needing re-creation drop from 45 to 14, fix wall-clock hours from 15.0 to 4.7; the number of fields that cannot be undone is 6.
Next Step
This lesson reduced instance launch to a single definition, but a person still makes the launch decision: the template exists, and so does whoever presses the button. The numbers from previous lessons showed that this button is pressed thousands of times a month — 1,450 interruptions on interruptible capacity, 180 instance changes over 90 days.
Producing a group of instances from a single template makes it possible to hand off two more jobs to that group: changing the count by demand, and renewing an unhealthy instance on its own. M22/K03 measured horizontal scaling and the oscillation born from metric lag; that scan is not repeated. The next lesson measures the combined behavior of these two jobs and asks: if scaling is also triggered while an unhealthy instance is being replaced, what does group size do, how many instances change at once, and in how many rounds does the group drop below capacity?
To keep your progress and take notes, Log in
My notes
Log in to take notes.