---
title: 'Virtual Private Cloud'
source: 'https://academia.sh/en/courses/compute-and-networking-in-the-cloud/virtual-private-cloud'
course: 'Compute and Networking in the Cloud'
language: en
updated: '2026-09-13T05:07:38+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

# 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.

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.

```js
// 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

```js
// 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

```js
// 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.
