Skip to content
academia.sh

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 /25 block and 1.5% of the /20 block; 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: /25 runs out in period 5, /24 in period 7, /23 in period 8. The wide side’s cost is collision: /16 hits one neighboring block, /14 two, /12 four, and a colliding block cannot be peered.
  • A short prefix shifts the base back to align; writing 10.42.0.0 with /12 builds a block starting at 10.32.0.0 and holds all 256 slices allocated to the organization. /20 holds one slice and leaves 151.
  • The irreversible item is the address block and prefix length. Writing /20 instead of /23 on 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close