Lesson 07 / 15
Virtual Private Cloud
Address space planning is counted item by item: as the prefix length is swept, how many addresses stay usable, how many are reserved, how many are wasted; which period the narrow choice runs out of growth in, which neighboring blocks the wide choice collides with, and how many units the cost of this item — unchanged once it is set up — comes to.
Published Updated
In this lesson
- Sweeps the address space size choice and works out the usable, reserved, and wasted address count for each prefix
- Measures the two separate costs of the narrow and wide choices: the overflow period and the count of items colliding with neighboring blocks
- Names the item that stays fixed once set up, and compares the wrong choice's three exit paths in fictional units
Contents
The machine was rented, its image was chosen, and its scaling was handed to a group. Every rented instance has an address, and that address comes from somewhere: an address space drawn before the instances ever launch. A virtual private cloud is the space reserved for a tenant, with address distribution inside it left to the tenant. The provider takes on the transport plane, isolation between tenants, and the address allocation mechanism; the block, the prefix length, and the block’s placement within the organization’s range are left to whoever writes them.
This item is set apart from the others in one way. A route rule that is written wrong gets fixed, a filter rule that is loose gets narrowed — an address block, once set up, does not change. Three things are asked: how many addresses a block actually makes usable, which period the choice runs short in, and how much the day it runs short costs.
- CN1. The eight-period growth series and the neighboring block list belong to the course’s
fictional regional measurement network. No real network is set up; what is measured is the model
built with
node. The address arithmetic is real. - CN2. All blocks are written from private address ranges; no real public block is used.
- CN3. The provider is assumed to reserve five addresses in every subnet. In reality this number varies by provider; it shifts the usable address count without changing its direction.
- CN4. The growth series is written by hand; there is no randomness, and every run produces the same numbers.
- CN5. The costs are in fictional units, not currency. They carry the ratio between the paths, not an absolute amount.
- CN6. The neighboring block list is cut down to seven. In reality the number of blocks registered at an organization is far higher; it raises the collision count without changing the sweep logic.
The Address Space Model
The model holds three things: a handful of functions that resolve a block with address arithmetic, the neighboring blocks registered under the same organization, and the measurement network’s eight-period demand. A demand row records how many zones a period spreads across, how many tiers it uses, and how many instances run; the subnet count is the product of the zone and tier counts.
// address.mjs — address space plan model; no real network is set up, address arithmetic is real export const toInt = (ip) => ip.split('.').reduce((a, o) => a * 256 + Number(o), 0); export const toIp = (n) => [16777216, 65536, 256, 1].map((b) => Math.floor(n / b) % 256).join('.'); export const block = (c) => { const [ip, e] = c.split('/'); const prefix = Number(e); const size = 2 ** (32 - prefix); const start = Math.floor(toInt(ip) / size) * size; // shorter prefix shifts the base back return { name: `${toIp(start)}/${prefix}`, prefix, size, start, end: start + size - 1 }; }; export const overlaps = (a, b) => a.start <= b.end && b.start <= a.end; // The same organization's existing blocks (fictional); all in private ranges. export const NEIGHBORS = [ ['central-hall', '10.0.0.0/16'], ['old-measurement', '10.8.0.0/14'], ['lab', '10.32.0.0/16'], ['field-temp', '10.41.0.0/16'], ['metric-archive', '10.42.128.0/17'], ['backup-collection', '10.44.0.0/14'], ['shared-connection', '172.20.0.0/14'], ].map(([name, c]) => ({ ...block(c), name })); export const SEED = '10.42.0.0'; // starting address the planner writes export const ORG = block('10.32.0.0/12'); // range allocated to the organization; new teams draw from here export const RESERVED = 5; // provider reserves five addresses in every subnet (CN3) // [period, zone, tier, instance] — the fictional measurement network's growth series (CN4) export const DEMAND = [ [1, 2, 2, 18], [2, 2, 2, 26], [3, 3, 2, 44], [4, 3, 2, 71], [5, 3, 3, 118], [6, 3, 3, 196], [7, 4, 3, 340], [8, 4, 3, 612], ]; export const subnetCount = (t) => t[1] * t[2]; export const required = (t) => t[3] + t[1] + 2; // instance + per-zone gateway + two balancers export const reserved = (t) => RESERVED * subnetCount(t); export const used = (t) => required(t) + reserved(t); export const overflow = (b) => (DEMAND.find((t) => used(t) > b.size) || [0])[0]; export const colliding = (b) => NEIGHBORS.filter((n) => overlaps(b, n)); export const TOTAL_SLICES = ORG.size / 4096; // how many /20 slices in the org range export const slices = (b) => { // the /20 slices the block holds const s = new Set(); if (b.end < ORG.start || b.start > ORG.end) return s; const first = Math.floor((Math.max(b.start, ORG.start) - ORG.start) / 4096); const last = Math.floor((Math.min(b.end, ORG.end) - ORG.start) / 4096); for (let i = first; i <= last; i++) s.add(i); return s; }; export const NEIGHBOR_SLICES = new Set(NEIGHBORS.flatMap((n) => [...slices(n)]));
The base calculation inside the block function is the model’s quiet claim: the address written
is not the block chosen. As the prefix shortens, the base shifts back to align, so writing
10.42.0.0 with a short prefix builds a block that starts somewhere else. slices opens a second
axis — it counts how many slices the block holds within the range allocated to the organization,
because the space taken is space no other team gets.
Sweeping the Prefix Length
// run1.mjs — prefix length is swept: how many are usable, when it overflows, what it hits import { block, SEED, DEMAND, subnetCount, required, reserved, used, overflow, colliding, slices, NEIGHBOR_SLICES, TOTAL_SLICES } from './address.mjs'; const last = DEMAND[DEMAND.length - 1]; console.log(`period-8 demand: ${last[3]} instances + ${last[1]} gateways + 2 balancers = ${required(last)} addresses; ` + `${subnetCount(last)} subnets x 5 = ${reserved(last)} reserved; total ${used(last)}`); console.log(`${'block'.padEnd(17)} ${'total'.padStart(8)} ${'usable'.padStart(14)} ` + `${'overflow'.padStart(11)} ${'waste'.padStart(8)} ${'overlaps'.padStart(9)} ${'/20 left'.padStart(10)} hits neighbor`); for (const prefix of [25, 24, 23, 22, 21, 20, 18, 16, 14, 12]) { const b = block(`${SEED}/${prefix}`), t = overflow(b), c = colliding(b); const withBlock = new Set([...NEIGHBOR_SLICES, ...slices(b)]); console.log(`${b.name.padEnd(17)} ${String(b.size).padStart(8)} ${String(b.size - reserved(last)).padStart(14)} ` + `${String(t ? `period ${t}` : '-').padStart(11)} ${String(t ? '-' : b.size - used(last)).padStart(8)} ` + `${String(c.length).padStart(9)} ${String(TOTAL_SLICES - withBlock.size).padStart(10)} ${c.map((k) => k.name).join(' ')}`.trimEnd()); }
period-8 demand: 612 instances + 4 gateways + 2 balancers = 618 addresses; 12 subnets x 5 = 60 reserved; total 678 block total usable overflow waste overlaps /20 left hits neighbor 10.42.0.0/25 128 68 period 5 - 0 151 10.42.0.0/24 256 196 period 7 - 0 151 10.42.0.0/23 512 452 period 8 - 0 151 10.42.0.0/22 1024 964 - 346 0 151 10.42.0.0/21 2048 1988 - 1370 0 151 10.42.0.0/20 4096 4036 - 3418 0 151 10.42.0.0/18 16384 16324 - 15706 0 148 10.42.0.0/16 65536 65476 - 64858 1 144 metric-archive 10.40.0.0/14 262144 262084 - 261466 2 112 field-temp metric-archive 10.32.0.0/12 1048576 1048516 - 1047898 4 0 lab field-temp metric-archive backup-collection
The reserved sixty addresses are the same in every row, not the share: in the /25 block, 47% of
the total goes to reservation, and in the /20 block, 1.5%. In the small block, the partitioning
scheme itself eats half the block, and that bite goes to no instance. The overflow column gives
the cost of the narrow side: /25 runs out in period five, /24 in period seven, /23 in period
eight. Period eight’s demand is 678 addresses, and the /23 block has 512.
The right side counts the cost in the opposite direction. The /16 block hits exactly one
neighbor, and that neighbor is 10.42.128.0/17 — an item sitting inside the planner’s own second
octet. Writing /14 shifts the base to 10.40.0.0, writing /12 shifts it to 10.32.0.0; in
both cases the address written is not the start of the block that gets built. The shift pulls two
more neighbors in. A colliding block cannot be peered: on both sides the same address would map to
two different machines, so the peering is refused and only address translation is left.
The last column is the wide choice’s invisible cost. The range allocated to the organization holds
256 slices; neighbors hold 104 of them. Every choice at /20 and below takes one slice and leaves
151, /18 leaves 148, /14 leaves 112, /12 leaves zero. Some other team will ask for a block
from the same range that same day. The gap between zero and 151 produces no warning today.
The sweep draws its own window: between /22 and /18, there is neither overflow nor collision.
/20 leaves 3,418 addresses empty in period eight — this waste does not correspond to a cost item,
it only holds one slice. The difference from /23 is a single character on setup day.
The Cost of the Narrow Choice
// run2.mjs — /23 was chosen; at overflow, three exit paths are counted in fictional units import { block, SEED, DEMAND, subnetCount, used, overflow } from './address.mjs'; const chosen = block(`${SEED}/23`), wide = block(`${SEED}/20`); const t = overflow(chosen), d = DEMAND.find((x) => x[0] === t); console.log(`selected block ${chosen.name}: ${chosen.size} addresses, period-1 demand ${used(DEMAND[0])} ` + `-> ${(chosen.size / used(DEMAND[0])).toFixed(1)}x headroom, setup day says nothing`); console.log(`writing ${wide.name} that same day costs extra: 0 units, 0 extra lines, 0 extra minutes`); console.log(`overflow period ${t}: ${d[3]} instances, ${subnetCount(d)} subnets, ${used(d)} addresses required, ${chosen.size} available\n`); // Costs are fictional "unit" (CN5); they carry ratios, not a currency. const PATH = [ ['rebuild', d[3] * 2 + subnetCount(d) * 15 + 5 * 40, 0, 0], ['second network + peering', 120, 95, subnetCount(d)], ['address translation layer', 40, 60, 2], ]; const baseline = PATH[0][1]; console.log(`${'exit path'.padEnd(25)} ${'setup'.padStart(8)} ${'per period'.padStart(11)} ` + `${'permanent item'.padStart(15)} ${'6 periods'.padStart(10)} ${'passes rebuild in'.padStart(23)}`); for (const [name, setup, perPeriod, permanent] of PATH) { let n = 0; if (perPeriod > 0) { n = 1; while (setup + perPeriod * n <= baseline) n++; } console.log(`${name.padEnd(25)} ${String(setup).padStart(8)} ${String(perPeriod).padStart(11)} ${String(permanent).padStart(15)} ` + `${String(setup + perPeriod * 6).padStart(10)} ${String(n ? `period ${n}` : '-').padStart(23)}`); }
selected block 10.42.0.0/23: 512 addresses, period-1 demand 42 -> 12.2x headroom, setup day says nothing writing 10.42.0.0/20 that same day costs extra: 0 units, 0 extra lines, 0 extra minutes overflow period 8: 612 instances, 12 subnets, 678 addresses required, 512 available exit path setup per period permanent item 6 periods passes rebuild in rebuild 1604 0 0 1604 - second network + peering 120 95 12 690 period 16 address translation layer 40 60 2 400 period 27
The second line is this lesson’s measure. Writing /20 instead of /23 costs zero units on setup
day: the same command, the same minute, the same line count. In period one, the /23 block showed
12.2x headroom, and that appearance held true for seven periods. In period eight the block runs
out, and once it does, one of three paths has to be chosen.
Rebuilding costs 1,604 units: moving 612 instances, rebuilding 12 subnets, and opening an interruption window. It is one-time and leaves no permanent item behind. Opening a second network and peering it costs 120 units, and it wins every meeting — after six periods it still stands at 690 units, cheaper than half the rebuild. It passes rebuild in period 16. The address translation layer starts even cheaper and passes rebuild in period 27.
The permanent item column carries the real difference. The second network leaves 12 permanent items: one row that has to be written into each of the twelve route tables and updated on every later change. The address translation layer’s two permanent items are not rows but properties — the two sides cannot see each other’s real address, and the source address in the logs turns into the layer’s address. The cost of delaying is not in the unit count; it is in carrying these two columns into every lesson that follows.
The Item That Stays
| Item | Held by | Changes later? |
|---|---|---|
| Address block and prefix length | team | no |
| The block’s placement within the org range | team | no |
| Zone count | team | only grows |
| Subnet boundaries | team | only grows |
| Route rules | team | yes |
| Filter rules | team | yes |
| Name resolution and flow logs | team | yes |
| Peering and private connections | team | yes, if no collision |
| Five addresses reserved per subnet | provider | — |
| Address allocation mechanism | provider | — |
| Transport plane and tenant isolation | provider | — |
| The gateway’s own redundancy | provider | — |
Eight of the twelve items stay with the team, four are handed off. The four handed off could be
said to never even be asked — the five reserved addresses are not a choice but a cut, and in the
/25 block it approaches half the total. Of the eight that stay, four change freely, two can only
grow, and two never change once set up.
“Unchanging” here is not a prohibition, it is a stack of dependencies. The moment the block is
written, three items come to depend on it: distributed addresses, route rules written against
those addresses, and filter rules written against the same addresses. Widening the block
invalidates all three at once, because widening shifts the base — turning the 10.42.0.0/23 block
into /20 is a new block, not a grown version of the same block. It also changes its place in the
organization’s record, and at that point which neighbors it hits has to be asked again.
The irreversible item is these two rows: the block itself and the block’s place in the organization’s range. When this course’s other items are written wrong, a rule gets fixed; when this one is written wrong, the result is a 1,604 unit migration or a permanent add-on that touches every route table. The moment the decision is made is the moment with the least information on hand — period one ran 18 instances, and period eight’s 612 instances were not written down anywhere yet.
Summary
- Period eight’s demand is 618 addresses plus 60 reserved addresses for 12 subnets. The same 60
addresses eat 47% of the
/25block and 1.5% of the/20block; the partitioning scheme’s cut is as decisive as the block itself in a small block. - The narrow side’s cost is the overflow period:
/25runs out in period 5,/24in period 7,/23in period 8. The wide side’s cost is collision:/16hits one neighboring block,/14two,/12four, and a colliding block cannot be peered. - A short prefix shifts the base back to align; writing
10.42.0.0with/12builds a block starting at10.32.0.0and holds all 256 slices allocated to the organization./20holds one slice and leaves 151. - The irreversible item is the address block and prefix length. Writing
/20instead of/23on setup day costs 0 units; by period eight, that same difference costs 1,604 units to rebuild, or 120 units plus 95 units per period and 12 permanent route rows for a second network. - The path that looks cheap is the permanent one: the second network passes the rebuild total in period 16, the address translation layer in period 27, and by then the end of direct addressing cannot be undone.
Next Step
The block is chosen, and it holds a demand of 678 addresses — but the block is still one piece. Addresses have to be split before they can be used, and the splitting scheme opens a new question: does every instance need to be directly reachable from outside. The next lesson’s question counts both axes of splitting at once: when the same block is split into a small number of large pieces versus a large number of small pieces, how many addresses stay usable; how many hundred addresses the five addresses cut per subnet hold across how many pieces; and how many instances a zone loss takes away under which arrangement — and how far closing off part of the pieces from outside cuts the directly reachable instance count while requiring which extra component in exchange.
To keep your progress and take notes, Log in
My notes
Log in to take notes.