Field guide · Nº 16
A field guide to servers, containers, and the cloud
Infrastructure conversations are unusually full of sentences that cannot be acted on. "The server is down" is the canonical one: four completely different failures, four completely different first moves, one word covering all of them. The imprecision is not laziness — it is the residue of a time when "server" really did name a single object you could point at in a room. That time is gone, and the word now names at least three things at once: a machine, an operating system on it, and a process bound to a port.
This module makes those three separable, because every abstraction the rest of the course introduces — virtual machines, containers, orchestrators, functions — is a way of rearranging exactly these pieces. It ends by diagnosing the most-quoted sentence in software, it works on my machine, which turns out not to be a joke but a precise statement about which layers of a program's environment are written down and which are not. We will diagnose it here and deliberately leave it unfixed until module 3, where the fix is the whole point of containers.
Run flask run on your laptop and you have a server. Not a metaphorical one — the real thing: a process that has asked the operating system for a port, is now listening on it, and will accept connections and return HTTP responses until you kill it. The machine in a data center running cellarbook's API is doing exactly this. It is bigger, it is somebody else's, and it is more carefully configured, but it is not doing a different kind of thing.
The word server is doing triple duty, and the three meanings fail independently. There is the machine — physical or virtual — with a CPU, memory, a disk, and a network interface. There is the operating system on it, which owns the hardware and hands out slices of it. And there is the process: your code, loaded into memory, holding a port. When someone says the server is down, the useful next question is which of the three they have evidence about, and the usual honest answer is "none — the page just didn't load."
Your production server is not a different species from flask run. It is the same arrangement — a process holding a port on a machine — with harder promises around it. Every technology in this course changes who supplies and maintains the layers under that process; none of them change the arrangement.
A machine has one network interface but many processes that want to talk. The kernel resolves this with ports: a 16-bit number attached to each connection, and a table mapping listening ports to processes. Port 443 belongs to nginx; port 8000 belongs to gunicorn; port 5432 belongs to Postgres. Nothing about these numbers is intrinsic — they are conventions plus a registry the kernel enforces one-at-a-time. Two processes cannot hold the same port on the same interface, which is the entire content of the Address already in use error you have seen a hundred times.
Binding has an address as well as a number, and this is where a whole family of bugs lives. Bind to 127.0.0.1:8000 and the kernel accepts connections only from the machine itself. Bind to 0.0.0.0:8000 and it accepts them on every interface, including the one facing the network. An app bound to loopback answers curl localhost:8000 perfectly from inside the box, but a connection from outside is refused the instant it arrives: the kernel has no listener on the public interface, so it replies with a reset — not slow, not silent, refused. (It goes silent only if a firewall is also dropping the packets, a second failure layered on top.) That is a feature: gunicorn should bind to loopback, because nginx is the only thing that should be allowed to reach it.
Four failures, four symptoms, four first moves. Process dead: connection refused immediately; systemctl status gunicorn or the process list. Port unreachable (firewall or security group): the connection hangs and times out with no refusal, because a dropped packet produces silence where a closed port produces a refusal; check the security group before you touch the app. Machine dead: nothing answers, including SSH and ICMP; check the provider's console, not the app. Path broken (DNS resolves to the wrong address, expired certificate, load balancer marking the target unhealthy): the machine and process are both fine and the failure is upstream of them; resolve the name yourself and try the origin directly. The difference between an immediate refusal and a hanging timeout is the highest-value single bit in this whole diagnosis, and it costs nothing to observe.
Guide Nº 14 followed a request from the browser to the edge: DNS resolution, TLS negotiation, the load balancer's choice. This module picks up where a packet has already arrived at a machine and asks what receives it. Module 7 splices the two tellings together into one end-to-end path.
Production is somebody else's machine plus a set of promises: power with battery and generator backup, cooling, redundant network transit, physical security, and someone who will walk to the rack. You are not renting a computer so much as renting those promises; the computer is the cheap part. Everything the rest of this course calls a "compute choice" is a decision about how many of those promises, and how much of the software above them, you buy rather than keep.
The path from flask run to a served application changes four things, each for a reason you should be able to state. The dev server becomes gunicorn. Flask's built-in server is single-process, unoptimized, and explicitly not intended for production; gunicorn runs multiple worker processes, so requests run in genuine parallel across the GIL and one wedged worker does not take the rest down with it. gunicorn gets nginx in front of it. nginx terminates TLS, serves the React SPA's static files without waking Python at all, buffers slow clients so a user on hotel wifi does not occupy a Python worker for thirty seconds, and enforces limits on request size and rate. The process gets a supervisor — systemd — so a crash restarts it and a reboot brings it back, rather than requiring a human to notice. The machine gets hardened: automatic security updates, no password SSH, a firewall that opens 443 and nothing else, and the app running as an unprivileged user that cannot write to its own code.
The Flask development server in production is not a style violation, it is a vulnerability. With --debug enabled it exposes an interactive debugger: an unauthenticated stranger who triggers an exception gets a browser console that executes arbitrary Python on your machine, as your app's user, with your database credentials in the environment. That is remote code execution reachable through the front door. Treat any flask run found listening on a production port as an incident, not a cleanup ticket.
Here is the bug, exactly as it happened. Cellarbook lets a user upload a scanned wine label as a PDF; the API renders a preview image of the first page with pdf2image and stores it. On the developer's laptop — macOS, Python 3.12 installed via Homebrew, and poppler present system-wide because some other Homebrew formula pulled it in — PDF uploads work. On the production VM — Ubuntu 22.04, Python 3.10, no poppler — PDF uploads return 500. Only PDFs. JPEG and PNG uploads, which Pillow handles with no external tool, are fine, which is why the feature passed review, passed CI, and shipped.
Nothing here is mysterious once you name the real object. An application's dependency closure is everything it actually needs in order to run: the operating system and its version, system libraries installed by the OS package manager, the language runtime and its exact minor version, installed language packages, environment variables, filesystem paths and their permissions, and available system users. requirements.txt records exactly one of those seven layers. pdf2image appears in it; poppler — the system package whose pdftoppm binary pdf2image shells out to in order to rasterize a PDF page — does not, because it was never a Python package. pdf2image is only a thin wrapper that runs that binary, so when it is missing from the PATH the call raises at runtime. The laptop had it by accident.
pdf2image is identical on both machines and shells out to a system binary that exists on only one of them.Notice what CI did not save you. The build ran on a machine provisioned to look like the developer's, so it reproduced the accident rather than the target. A test suite validates behavior given an environment; it cannot validate the environment unless the environment is itself an artifact under test. That is the sentence to hold onto, because module 3 makes the environment an artifact and this bug stops being possible.
The tempting fixes are all bad. Installing poppler on the production box by hand fixes this machine and no other, and creates a machine whose configuration exists only in one person's shell history. Rejecting PDF uploads discards a real requirement to avoid a diagnosis. Pinning Python in a README documents an intention rather than enforcing one. We will carry this bug to module 3 and dissolve it there.
Module 1 left you with a machine, an OS, and a process. This module takes the machine — the layer that seemed most solid — and shows that it is software too. A hypervisor sits between physical hardware and operating systems and hands each OS a convincing imitation of a whole computer: its own kernel, its own disk, its own network card, its own view of memory. The OS does not know. It cannot tell.
That is the first instance of the move this entire course is about: slice the machine thinner, hand a layer to someone else. Virtualization is what turned a capital purchase with a six-week lead time into a line item you can create in forty seconds and destroy before lunch. Every economic fact about cloud computing descends from it. And when you rent an "instance," you are renting exactly this and nothing more mysterious — a fact that becomes useful the first time an instance disappears and takes your files with it.
A hypervisor is a thin layer of software that owns the physical hardware and presents virtual hardware to guests. Each guest is a virtual machine: a complete machine-as-far-as-it-knows, booting its own kernel, mounting its own root filesystem, believing its virtual CPUs and virtual network card are real. The hypervisor traps privileged operations and mediates them, with the host CPU's virtualization extensions doing most of the enforcement in hardware rather than in software — which is why the performance penalty is small enough that nobody argues about it any more.
The property that matters commercially is the strength of the boundary. Because each VM runs its own kernel and the hypervisor exposes only a narrow, hardware-mediated interface, a compromise inside one guest does not reach its neighbors. That is what makes multi-tenancy possible: your instance and a stranger's instance can sit on the same physical box, and the industry is willing to bet regulated workloads on the divider. Keep this in view — module 3's boundary is genuinely weaker, and the difference is the single most important comparison in the first half of this course.
Before virtualization, buying compute meant buying a computer. The unit was a physical box, the lead time was a procurement cycle, the commitment was years of depreciation, and the utilization was terrible — every application sized for its annual peak, idling the rest of the time, because sharing a box between two teams' applications meant sharing an OS and every conflict that came with it.
Virtualization changes the unit of sale. A provider buys one 64-core host and sells it as thirty independent instances to thirty unrelated customers, each of whom gets a machine-shaped thing they can reboot, reinstall, and destroy without consulting anyone. Utilization goes up because peaks do not coincide; provisioning drops from weeks to seconds because creating a VM is a software operation; and — the consequential part — billing becomes temporal. You can rent a machine for an hour. Everything that reads as "cloud-native" downstream of this is a consequence: autoscaling is only interesting because capacity is purchasable in minutes, and disposable infrastructure is only sane because destroying a machine costs nothing but the end of its billing.
This is why the responsibility spectrum in module 6 is also a pricing spectrum. Each slice thinner is also a smaller billable unit: years for a purchased server, hours for a VM, seconds for a container's node time, milliseconds for a function invocation. Module 9 shows how directly the shape of the bill follows from where you sit.
When you rent a 2 vCPU / 4 GB instance, you are renting a VM of that shape from a menu. Three pieces of fine print decide how much of what you assume is true.
A vCPU is a scheduled share, not a soldered core. It is a thread of execution that the hypervisor schedules onto physical cores. On general-purpose instance types you get a consistent share; on burstable types you get a small baseline — often a fraction of a core — plus a credit balance that lets you run at full speed for a while. Burstable instances are excellent for spiky, mostly-idle workloads and treacherous for sustained ones, because when the credits are exhausted the instance drops to baseline and every latency chart falls off a cliff at a moment unrelated to any deploy. "It was fine for three weeks and then got slow at 4 p.m. Tuesday" is often exactly this.
Instance families are a shape menu. The provider offers compute-optimized, memory-optimized, storage-optimized, and general-purpose shapes because real workloads have lopsided appetites; cellarbook's nightly valuation job is CPU-hungry and memory-light, while its Postgres wants memory above all. Renting the wrong shape means paying for a resource you do not use in order to get enough of the one you do.
Storage divides into two categories, and the division is unforgiving. Instance-local storage is physically attached to the host, is fast, and ceases to exist when the instance stops or fails — the data is not deleted so much as never durable in the first place. Network-attached volumes live in a separate replicated storage service, survive instance termination, can be detached and re-attached to a replacement, and can be snapshotted. Anything you would be unhappy to lose belongs on a network volume, in a managed database, or in object storage.
| Storage | Survives instance stop | Survives host failure | Typical use |
|---|---|---|---|
| Instance-local (ephemeral) disk | No | No | Scratch space, caches, temp files during processing |
| Network-attached volume | Yes | Yes (replicated) | Root filesystems, database data directories |
| Volume snapshot | Yes | Yes | Backups, machine images, restore points |
| Object storage | Yes | Yes (multi-AZ) | Uploaded files — cellarbook's label images belong here |
Here is the first of the four deployments this course will compare. All five cellarbook components go on a single 2 vCPU / 4 GB instance: nginx serves the React build and reverse-proxies gunicorn; gunicorn runs the Flask API; Postgres runs on the same box against a network-attached volume; cron fires the nightly valuation job; and label images are written to the local filesystem. DNS points app.cellarbook.com at the instance's address. Roughly $50 per month (illustrative): about $35 for the instance, $8 for the volume and snapshots, $7 for egress.
This deployment deserves more respect than it usually gets. It is $50 a month, it fits in one person's head, every debugging tool works the way the documentation says, and for a product with two thousand daily users it is genuinely adequate. Its weaknesses are equally honest. The nightly valuation job and the API share two vCPUs, so a twenty-five-minute batch process and your p99 latency are the same conversation. One runaway query takes down the product rather than one endpoint. And the volume holds both the database and 12 GB of customer label images, so a single storage failure or a fat-fingered rm is a two-category data loss.
You own this instance from the kernel up. When a privilege-escalation CVE lands in the Linux kernel, the provider patches the hypervisor and the physical host; the guest kernel is yours, and an unpatched instance is your finding in your audit. This is the first appearance of the frontier module 6 formalizes: the shared responsibility model is not cloud-provider marketing, it is a document that assigns control ownership, and on a rented VM it assigns almost everything above the hypervisor to you. Ask, of any compute choice, the auditor's version of the question — for each control, who produces the evidence?
Virtualization solved the machine problem and left the environment problem open. You could create a VM in seconds, but the thing that made your application run was still a set of decisions made once, by hand, inside that VM — apt installs, environment variables, directory layouts — and nothing in the artifact recorded them. Golden machine images were the attempted answer: capture the whole configured VM, gigabytes of it, and clone it. They worked, slowly, and only within one provider's ecosystem.
Containers close the gap by attacking it from the other side. Instead of virtualizing a machine, they isolate a process and give it a filesystem that is the recorded environment. The result is small enough to build on a laptop in thirty seconds, push over a network in a few more, and run identically on any Linux host in the world. This module builds the mechanism from the kernel up, retires the "lightweight VM" mental model that causes most container mistakes, and finally kills the PDF bug from module 1.
A container is an ordinary Linux process. Run one and the host's process table shows a process, owned by a user, consuming memory, schedulable like anything else. There is no guest kernel, no virtual hardware, no boot. What makes it a container is that the kernel has been asked to lie to it about two things.
Namespaces and cgroups are the whole trick. Namespaces scope what a process can see: a mount namespace gives it a different root filesystem, a PID namespace makes it believe it is process 1 and that its siblings do not exist, a network namespace gives it its own interfaces and port space, plus user, IPC, and UTS namespaces for identity, shared memory, and hostname. Cgroups cap what it can use: this process tree gets 0.5 CPU and 512 MB of memory, and the kernel enforces it. Namespaces are the walls; cgroups are the meter.
Because there is no kernel to boot and no hardware to emulate, a container starts in milliseconds and its overhead is roughly that of the process itself. And because there is no second kernel, the isolation boundary is a kernel-enforced boundary rather than a hardware-enforced one — the container and the host share one kernel, and a kernel vulnerability is a shared vulnerability. That is the honest trade, and it is why the phrase "lightweight VM" is worth actively refusing: it is a metaphor that predicts the performance correctly and the security incorrectly, which is the worst combination a metaphor can have.
Containers do not virtualize a machine. They isolate a process and hand it a filesystem. Speed, density, and the weaker security boundary are all the same fact stated three ways: there is no second kernel.
An image is a stack of read-only filesystem layers plus metadata about how to start the process. Each instruction in a Dockerfile produces one layer containing the filesystem changes that instruction made; the runtime stacks them with a union filesystem and adds a thin writable layer on top for the running container. Two consequences follow immediately, and both are why the format won.
Layers are cached and content-addressed. Rebuild after changing only your application code and every layer below the code layer is reused untouched — the base image, the apt packages, the pip install. Order your Dockerfile so the things that change least often come first and rebuilds cost seconds; order it badly and every one-character commit reinstalls your entire dependency tree.
Layers are shared across images. If twelve of your services build on python:3.12-slim, that base is stored once on a host and transferred once from the registry. Pulling the thirteenth image means pulling its unique layers only.
Now the PDF bug dies. Here is cellarbook's API image as a layer plan:
FROM python:3.12-slim # layer 1 — OS userland + the exact runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
poppler-utils libpq5 \ # layer 2 — the system tools, finally recorded
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt . # layer 3 — the dependency manifest, alone
RUN pip install --no-cache-dir -r requirements.txt # layer 4 — installed packages
COPY . /srv/cellarbook # layer 5 — application code (changes every commit)
USER cellarbook
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]Read module 1's Figure 1.2 against this listing. Every divergent row is now a line in a file: the OS userland and Python version are pinned by layer 1, the missing system tools are installed by layer 2, and the Python packages are pinned by layers 3–4. The dependency closure stopped being tribal knowledge and became a build artifact — which means it is in version control, it is code-reviewed, and it is identical on the laptop and in production because it is the same bytes, not a similar description.
A registry is the store images are pushed to and pulled from. It is unglamorous and it is the piece that makes "build once, run anywhere" mean something operationally: the artifact built on a laptop or in CI is uploaded once, and every machine that will run it pulls those exact layers. VMs never got here. Golden images were provider-shaped, multi-gigabyte, slow to build and slower to move, so in practice teams shipped scripts that configure machines rather than machines — and scripts drift, because they run at different times against different upstreams.
The fine print is the difference between a tag and a digest. A tag is a mutable label — cellarbook-api:latest is a pointer someone can repoint at any moment, and routinely does. An image digest is the SHA-256 hash of the image's content: cellarbook-api@sha256:9f3c… names one specific set of bytes and can never name a different one. Deploy by tag and "the same version" is a social convention. Deploy by digest and it is arithmetic.
:latest means "whatever was pushed most recently," which is not the same as "the newest release," and each host resolves it at pull time. Two machines running :latest can be running different builds indefinitely, and the drift is invisible until behavior diverges. Resolve tags to digests at deploy time and record the digest in the deployment — this is also the difference between an incident review that can state what was running and one that cannot.
Cellarbook's staging and production both run cellarbook-api:latest. Staging pulled at 14:02 after a merge; production pulled at 09:15 that morning, before it. A bug reproduces in staging and not in production, three hours evaporate, and the answer is that they are two different builds wearing one name. The fix is not discipline about pushing — it is that the deploy record says @sha256:9f3c…, so "what is running" is a question with an answer.
Everything good about containers came from removing the guest kernel, and so does the one caveat that matters. A container escape needs a kernel bug — a flaw in a syscall, a namespace, or a cgroup implementation — rather than a hypervisor bug. Kernels are enormous and the syscall surface reachable from inside a container is broad. Escapes are not everyday events, but they are a materially larger target than hypervisor escapes, and that difference should decide architecture in exactly one situation: when the code inside the container is not trusted.
Draw the line by asking who wrote the code. Your own services, built by your own team from your own repository, run fine in containers on shared hosts; the container boundary is there to prevent accidents, and the code is not adversarial. Arbitrary customer-supplied code — a CI runner executing pull requests from strangers, a notebook platform, a plugin sandbox, an AI agent executing generated code — is adversarial by construction, and the correct boundary is the VM: one microVM per tenant per execution, or at minimum a dedicated host per tenant. Providers of these products all converge on this answer, and they converge on it after incidents.
Notice that containers-on-VMs is therefore not redundancy, it is the standard arrangement. Your containers run on nodes that are themselves virtual machines; the hypervisor divides the provider's physical fleet between customers, and the container runtime divides your VM between your own workloads. Two boundaries, two purposes, both load-bearing.
In threat-model terms: the container boundary is a strong integrity and blast-radius control between components under one owner, and a weak tenancy control between mutually distrusting parties. If your model has an adversary inside the workload, do not spend the container boundary on them — spend a hypervisor. And when an auditor asks how customer workloads are separated, "containers on shared nodes" and "a VM per tenant" are different answers with different evidence behind them.
One container on one machine needs no orchestrator. You start it, and if it dies, systemd starts it again. The problem orchestration exists to solve appears at a specific and identifiable moment: when you have more containers than you can place by hand, on more machines than you can remember, and the placement has to change without a human present.
This module teaches Kubernetes as a set of ideas rather than a set of files. You will not see a line of YAML here, deliberately — YAML is the surface, and the surface changes; the ideas underneath (declared desired state, controllers reconciling forever, a scheduler solving a fitting problem, a stable name in front of unstable things) have been stable for a decade and are what let you read someone's cluster diagram and predict its behavior. The module ends by pricing the thing honestly, because the most common Kubernetes mistake is not a technical one.
Module 3 gave you a portable artifact. Deploying it to one machine is now easy. Deploying it to fifteen machines, keeping three copies of the API running at all times, replacing them when a machine dies at 3 a.m., and routing traffic only to the copies that are actually healthy — that is four separate problems, and they are the four an orchestrator automates:
Each is trivially handled by a human at small scale and impossible past a threshold. The threshold is lower than engineers expect for placement — you lose track of what's where at around a dozen services — and much higher than they expect for restart, because systemd handles single-machine restarts perfectly well. Kubernetes is the system whose vocabulary everyone now speaks for these four jobs. The vocabulary is what this module teaches; the configuration language is a detail you can learn in an afternoon when you actually need it, and learning it first is how people end up operating a cluster they cannot reason about.
Kubernetes' central idea is a single inversion, and everything else in the system is an application of it. You do not tell the cluster what to do. You tell it desired state — what should be true — and controllers make the world match, continuously, forever.
Declaring "three replicas of cellarbook-api at image digest sha256:9f3c…, each requesting 0.5 CPU and 512 MB" is a statement about the world, not an instruction. A controller then runs a loop that never ends: observe the actual state, diff it against the declaration, act to close the gap. That loop is reconciliation, and it explains behaviors that otherwise look like separate features. Self-healing is not a subsystem — it is the diff being nonzero after a pod dies. Rolling updates are the same loop with a changed declaration and a policy about how fast to converge. Scaling is the same loop with a changed replica count.
The practical consequence is that the declaration outranks the world. Delete a pod by hand and it comes back within seconds, because you changed the world and not the declaration; the controller observed a difference and did its only job. Engineers who arrive with imperative habits experience this as the cluster fighting them. It is not fighting; it is doing exactly what it was told, and the fix always goes in the declaration.
When a controller decides a pod should exist, the scheduler decides where. A pod — Kubernetes' unit of placement, one or more containers started, scheduled, and replaced together — declares what it needs: 0.5 CPU, 512 MB of memory, perhaps a particular kind of node. Each node advertises capacity. The scheduler solves a bin-packing problem, filtering nodes that cannot fit the pod and then ranking the survivors on spread, affinity, and utilization.
This makes Pending a precise and useful signal rather than an error. A pod is Pending because nothing fits: not enough free CPU or memory anywhere, or a constraint (a node label, a zone requirement, a volume that only attaches in one AZ) that no node satisfies. The corrective follows directly from which of those it is, and the diagnostic move is always the same — read what the scheduler says it filtered on. Note also that requests are what the scheduler uses, and requests are a claim you make, not a measurement: request 2 CPU for a service that uses 0.1 and you will pay for a cluster that is 5% utilized and still refuses to place your next workload.
Pods are disposable by design. They are created and destroyed by reconciliation constantly — on deploys, on scale events, on node failures — and each gets a fresh address. This is a problem for anyone who needs to call them, and Kubernetes' answer is the Service: a stable name and address in front of a set of pods selected by label, not by identity.
The Service says, in effect, "anything labeled app=cellarbook-api is a valid target." Pods matching that label join the set as they become ready and leave it when they stop passing health checks. Callers resolve one name — cellarbook-api — and the routing layer picks a healthy member. The set churns underneath continuously; the name never changes.
This is the same architectural move as a load balancer's virtual address in Guide Nº 14, applied inside the cluster, and it is the mechanism that makes "cattle, not pets" operationally possible. Without it, every caller would need to track the current membership of every callee, which is a distributed-consensus problem nobody wants to solve per-service.
Now the honest part. Adopting an orchestrator does not remove operational work; it exchanges one kind for another, and whether that is a good trade is a question about your specific situation that fashion cannot answer.
What you delete: manual placement, manual restart, manual replacement, hand-maintained routing, and — significantly — the class of outage where a machine dies at 3 a.m. and someone has to wake up. What you adopt: a distributed system that is itself a system you operate. A control plane (API server, scheduler, controllers, and the datastore holding declared state) that must be available and upgraded. A version treadmill with a support window measured in months, meaning a recurring upgrade project forever. A configuration surface large enough that misconfiguration is the leading cause of cluster incidents. Its own failure modes — scheduling deadlocks, resource-exhausted nodes, misconfigured admission control, certificates that expire on a schedule nobody diarized. And an access-control model you must actually design, because the defaults are permissive by intent.
A managed control plane moves part of this — the control plane's availability, patching, and upgrade execution — to the provider for a fee (~$75/month, illustrative). It does not move your workloads' configuration, your node images, your resource requests, or your understanding.
Cellarbook on managed Kubernetes: a managed control plane, three 2 vCPU / 4 GB nodes, three API pods, a worker deployment running both the label-image and nightly valuation jobs, managed Postgres outside the cluster, a cloud load balancer, a container registry. ~$270/month (illustrative): control plane ~$75, nodes ~$105, managed Postgres ~$60, load balancer ~$20, registry and miscellany ~$10. What breaks first at two thousand daily users? Nothing — which is precisely the finding. The cluster is comfortable, and what it consumes is the team's attention: the upgrade treadmill, the configuration surface, and the hours spent learning to debug it. Paged: the provider for the control plane and node hardware; you for everything running on it.
Symptom: a cluster whose replica counts never change, whose autoscaler has never fired, and whose adoption memo argues from readiness ("so we can scale") rather than from a problem anyone has had. Cost: roughly $220/month over the PaaS alternative, plus the upgrade treadmill and the weeks of learning, spent by a team that has not yet found product-market fit. Corrective: state the toil you are deleting in hours per month, before adopting. If you cannot name it, you are buying optionality with the only currency a small team has — attention — and the honest move is a PaaS until placement, replacement, or routing is a problem you actually have.
Each module so far has moved one layer across the frontier. Virtualization gave away the physical machine. Containers gave away the environment's assembly. Orchestration gave away placement and replacement. Serverless gives away everything that is left below your function: the machine, the operating system, the runtime, the process lifecycle, and scaling itself. You ship a handler. The provider does the rest.
This is the far right end of the spectrum and it is genuinely remarkable for the workloads it fits — a queue-driven image job that runs eleven times an hour costs approximately nothing and requires approximately no operations. It is also the position where the constraints are most rigid, because every one of them is the direct price of something you handed over. This module walks the constraints in order, adds the middle position (managed platforms) that most teams should actually consider first, and is honest about what you can no longer control.
A serverless function is a handler — one function, with a defined signature, in a supported runtime — that the platform runs in response to events. There is no resident process. When a request arrives and no instance of your handler is warm, the platform creates one: allocates a sandbox (usually a microVM, which is module 3's isolation argument reappearing as a product decision), fetches your code, starts the runtime, runs your initialization, then calls the handler. When traffic stops, instances are recycled and your footprint is zero. When a hundred requests arrive at once, the platform creates a hundred instances without asking you.
Billing follows the same logic: you pay for invocation-time — roughly, memory allocated × milliseconds executed — plus a per-request fee. Idle costs nothing. This is not a discount so much as a different pricing axis, and module 9 shows where it crosses.
What crossed the frontier in that one move: the machine, the OS and its patching, the runtime and its patching, the process supervisor, the capacity decision, and the scaling policy. What stayed: your code, your configuration, your data model, your bill, and your understanding of all four.
Everything you handed over has a price, and each price has a name.
Cold starts. The first invocation on a new instance pays for the setup the platform did on your behalf: sandbox allocation, code download, runtime start, and your own initialization code. Typical range is roughly 100 ms for a small handler in a fast runtime to several seconds for a large deployment package, a heavy framework, or a JVM. Warm invocations skip all of it and run in single-digit milliseconds. The consequence is a latency distribution with two distinct populations rather than a smooth curve, so a p50 that looks excellent can coexist with a p99 that stalls — and the worst-affected user is reliably the first one after a quiet period, which in a demo is whoever is watching.
Statelessness. Instance memory and local disk do not persist meaningfully between invocations. They sometimes appear to — a module-level variable set on one invocation is still there on the next if the same warm instance handles it — which is worse than if they never did, because it produces code that passes every test and fails in production at a rate proportional to your traffic.
Execution caps. Platforms bound how long a single invocation may run — commonly on the order of fifteen minutes. Anything longer must be decomposed or moved.
Connection limits. Each concurrent instance is a separate process wanting its own database connection. A hundred concurrent invocations means a hundred connection attempts against a Postgres instance configured for a hundred total, and the failure looks like a database outage while the database is perfectly healthy. The fix is a connection pooler between functions and database — a component you must remember to include, because nothing warns you until the traffic arrives.
"Serverless scales automatically" is true about your compute and silently false about everything your compute talks to. The function scales to a thousand concurrent instances; your database does not, your third-party API's rate limit does not, and your downstream service's connection pool does not. Automatic scaling in one tier relocates the bottleneck to the next tier down, and it arrives there with no warning because the tier that used to throttle it — a fixed number of resident processes — is exactly what you removed.
Between "you run processes on machines" and "you ship handlers" sits the position most teams should evaluate first and usually skip. A PaaS takes an application — a repository, a container image, a build command — and runs it as resident processes on infrastructure you never see. It builds from source, provisions a runtime, runs your web processes and worker processes, patches everything underneath, restarts on crash, and scales when you tell it to (or on a rule you set).
The critical difference from functions is resident. Your processes stay up, so there is no per-request cold start and no statelessness surprise inside a request's lifetime; a long-running job is just a worker process, not an execution-cap problem. The critical difference from a VM is that you never touch the machine: no kernel, no OS packages, no supervisor configuration, no TLS certificate management.
What you pay: a premium over the raw compute underneath — Variant 4 costs about $110/month for what would be roughly $50 of instances — and the platform's opinions. It supports the runtimes it supports, the build process it prescribes, the scaling controls it exposes, and the observability it surfaces. When your need falls inside those opinions, this is the best value on the spectrum for a small team. When it falls outside, there is no escape hatch, and that is the whole risk in one sentence.
PaaS instances have writable local disks that are discarded on every deploy, restart, and scale event. Cellarbook's Variant 4 therefore cannot keep label images on local disk; they must move to object storage before the first deploy. This constraint is usually experienced as an annoyance and is actually the platform enforcing module 8's lesson early — the app was already broken for horizontal scaling, and the PaaS simply refused to hide it.
Move right and you lose access, not just responsibility. There is no shell on the machine — you cannot log in and look, and the entire family of debugging techniques that begins with "ssh in and check" is gone. There is no kernel tuning, no custom system library outside what a supported build process installs, no control over which host your code lands on or how long it lives there. Observability is whatever the platform exports: if it does not emit the metric, that metric does not exist for you, and correlating a latency anomaly with an underlying host event may be simply impossible because you are not told about host events.
Vendor coupling is real here and worth naming without drama. Your handler signature, event formats, identity model, and the managed services it calls are provider-shaped; moving is a rewrite of the edges, not a redeployment. That is often an acceptable trade — the alternative is building and operating the displaced layers yourself, permanently, to preserve an option you may never exercise. Module 9 examines what that option actually costs when teams buy it early.
Here is the upside auditors care about. On a VM you evidence OS patching yourself: an inventory, a patch cadence, scan results, a remediation SLA. On functions, the OS and runtime are patched by the provider, and the control becomes inherited — you evidence it by citing the provider's attestation (their SOC 2 report and the shared-responsibility matrix's assignment of that control) rather than by producing scan output. The audit surface shrinks with the responsibility, and it shrinks in a way that is easy to defend because someone else is already producing the evidence at scale. Two cautions: inherited is not absent — you must still evidence that you are entitled to inherit it, which means the provider's report in your file and the relevant control mapped in your matrix. And the controls that remain yours (identity, secrets, data handling, application security) become a larger fraction of your surface, so "we moved to serverless" is a reduction in scope, not a reduction in diligence.
Five modules have introduced five technologies, and if they were five separate topics this course would have failed. They are one topic. Each is the same trade executed at a different depth: take a layer you currently manage, hand it to someone else, and pay for it in money, abstraction, and new failure modes.
This module assembles the pieces into the grid the rest of your career can use. It is the course's central artifact and it is deliberately unglamorous: columns for positions, rows for layers, every cell marked you or provider. You already know this grid from another context — it is the shared responsibility model, the document that decides which controls you evidence and which you inherit. That is not an analogy. It is the same grid, read by a different department.
Look back at what each module actually did. Virtualization took the physical machine — procurement, racking, hardware failure, capacity planning — and handed it to a provider, paying in a small performance overhead and the new failure mode of a noisy neighbor. Containers took environment assembly and handed it to a build artifact, paying in a weaker isolation boundary and a new class of build-and-registry concerns. Orchestration took placement, restart, and replacement and handed them to a control loop, paying in a large configuration surface and a platform that is itself a system to operate. Serverless took the machine, OS, runtime, and scaling and handed all of them over at once, paying in cold starts, statelessness, execution caps, and lost access.
Same move, four depths. Which means the interesting question was never "what is the modern choice." The word modern is a claim about chronology masquerading as a claim about fitness, and it should be retired from this conversation entirely. Serverless is newer than VMs and is the wrong choice for a 200 req/s steady workload; VMs are older than everything and are the right choice for a database primary. Age predicts nothing. The real question is a single sentence, and it is the one this whole course exists to make askable: what do you want to stop managing, and what are you willing to pay for that?
There is no ladder, only a spectrum. Positions differ in what you manage, what you pay, and which failure modes you inherit. A team that can name what it wants to stop managing makes this decision in an afternoon; a team that cannot makes it by fashion and discovers the bill and the pager rotation afterwards.
Here is the grid. Read a column as a job description: everything marked YOU is work your team performs and evidence your team produces.
Three cells deserve reading closely, because they are where the grid is usually filled in wrong. Managed Kubernetes, OS row: split. The provider runs and patches the control plane; you own the node images, which means node OS patching and node upgrades are yours unless you have specifically bought a managed node service — and "managed Kubernetes" is regularly misread as covering both. Managed Kubernetes and PaaS, scaling row: also split, and differently. The mechanism is the provider's; the policy is yours — you set the replica counts, the target metrics, and the minimums, and a badly set policy produces a badly scaled system on infrastructure that worked perfectly. The bottom row: never crosses. Your application's correctness, its security, and your data are yours at every position, which is why "we moved to serverless" is never an answer to an application-security question.
You have administered this grid before under a different name. The shared responsibility model is a provider's formal statement of which controls belong to them and which belong to the customer, and it is the document an auditor reaches for when deciding whether your evidence package is complete. It is the same grid as Figure 6.1. The engineering question — who maintains this? — and the compliance question — who signs this control? — and the operational question — who gets paged? — are one question that three departments ask in three vocabularies.
Read a column as an audit scope. On a VM, patch management is a control you own: you produce the asset inventory, the patch cadence, the vulnerability scan output, and the remediation SLA with evidence of adherence. Move to serverless and OS patching becomes inherited: you evidence it by citing the provider's attestation and the matrix cell assigning it to them. The scope shrank because the responsibility shrank, and the two shrink together always.
Two disciplines keep this honest. Inherited is not absent. You must still demonstrate entitlement to inherit — the provider's report on file, in force, covering the services and regions you actually use, and the control mapped in your matrix. An inherited control with no attestation in the file is an unevidenced control. The remainder concentrates. Identity, secrets, data handling, application security, and configuration are yours at every position, so as the surface shrinks they become a larger fraction of it. Moving right reduces scope, not diligence.
Two failure modes you have seen from the other side of the table. The first is the team that says "the cloud is SOC 2 compliant, so we are" — a category error the grid dissolves instantly, because their attestation covers their cells and yours covers yours. The second is subtler and more expensive: an organization inherits a control correctly and never re-verifies the inheritance, so when the provider's report scope changes, or a workload moves to a service the report doesn't cover, the control quietly becomes unevidenced with nothing on the surface to indicate it. The corrective for both is the same and it is boring: name the cell, name the owner, name the evidence, and re-check the mapping when either the architecture or the report changes.
The last deployment. Variant 4 — cellarbook on a PaaS: two web instances running the Flask app, one worker instance for both jobs, managed Postgres as a platform add-on, the React SPA served from the platform's static hosting. The platform builds from the repository, deploys, patches everything underneath, and restarts on crash. ~$110/month (illustrative). One constraint bites before the first deploy: the filesystem is ephemeral, discarded on every deploy and restart, so label images must move to object storage. That is module 8's lesson arriving two modules early, enforced by a platform that refuses to let the app pretend.
Now the course's central exhibit, assembled. One product, five components, four positions.
| V1 — one VM | V2 — managed k8s | V3 — serverless | V4 — PaaS | |
|---|---|---|---|---|
| Frontier sits at | the hypervisor | the control plane and node hardware | the handler signature | the runtime |
| Monthly (illustrative) | ~$50 | ~$270 | ~$25 | ~$110 |
| You patch | kernel, OS, runtime, deps | node images, runtime, deps | deps only | deps only |
| Breaks first | CPU contention; one disk holds everything | nothing at this scale — the team's attention instead | cold starts; the nightly job's execution cap | the bill's slope; needs outside the platform's opinions |
| You get paged for | everything, including the kernel | everything running on the cluster | code, configuration, and the bill | the app |
| Where the label images live | local disk (fragile) | object storage | object storage | object storage (forced) |
Read across the rows rather than down the columns. The application never changed. What changed is where the line falls, and every consequence — the bill, the pager, the failure mode, even where the customer's photographs are stored — follows from that one placement.
Every position on the spectrum runs somewhere physical, and where that somewhere is turns out to be one of the few architectural decisions that is genuinely hard to change later. This module gives you the map: regions, availability zones, and the failure domains they define.
It also closes a loop. Guide Nº 14 followed a request from a browser to the edge — DNS resolution, TLS, the load balancer's choice of target — and stopped there. This module continues the same journey inland: from the edge, across zones, into a cluster, to one specific container, and to the database behind it. Same product, same hostname, same origin. What is new here is the question asked at each hop: if this dies, who notices?
A region is a geographic area — roughly a metro area — containing multiple data centers. An availability zone is one or more of those data centers operated as an isolated failure domain: independent power feeds and generators, independent cooling, independent network transit. Zones inside a region are close enough for fast private links (roughly 1–2 ms round trip) and far enough apart that a flood, a fire, a power event, or a bad electrical maintenance procedure hits one and not its siblings. Between regions the distance is real: tens of milliseconds within a continent, well over a hundred across an ocean.
The definition that does the work is what fails together. A zone is the unit in which correlated failure is expected. Everything you know about resilience reduces to placing components in different failure domains and then verifying that you actually did. Note also that "AZ" is a per-account label: your AZ named us-east-1a and another customer's us-east-1a may be different physical zones, which is a deliberate anti-herding measure and a good reason to reason about zones by count and distinctness rather than by name.
| Scope | What fails together | Latency between | What survives it |
|---|---|---|---|
| One machine | every process on it | — | replicas on another machine |
| One rack | machines sharing power and top-of-rack network | <1 ms | spread within the zone |
| One availability zone | all racks in it — power, cooling, transit | 1–2 ms to siblings | multi-AZ deployment |
| One region | all zones — rare, but flooding, fiber cuts, and control-plane failures have done it | 20–150+ ms to other regions | multi-region, at significant cost |
Surviving the loss of an availability zone requires three things, and the phrase "multi-AZ" is routinely applied to deployments that have one or two of them.
One: compute in more than one zone, with enough capacity in the survivors to carry the load alone. Three zones at 34% utilization each survive a zone loss; three zones at 80% survive it by browning out, because two zones cannot serve 240% of their capacity. Two: a load balancer that spans zones and health-checks across them, so traffic stops going to the dead zone automatically. A balancer that itself lives in one zone is a single point of failure wearing a resilience label. Three: state replicated across zones, with a defined failover procedure. This is the one that gets skipped, because it is the expensive one — a multi-AZ managed database roughly doubles its cost, and the standby does no useful work on an ordinary day.
Symptom: an architecture diagram showing compute in three zones and exactly one database icon with no zone label. What actually happens in a zone outage: if the database is in the failed zone, every surviving API instance is healthy, passing health checks, receiving traffic, and returning 500s — arguably worse than being down, because the load balancer sees healthy targets and keeps routing. Corrective: audit the claim component by component and place state first; compute is the easy part and it is the part people draw.
Guide Nº 14 traced a request from the browser to the edge. Pick it up there. A user opens app.cellarbook.com; DNS has resolved, TLS has been negotiated, and the request is at a CDN edge location near the user. Everything from here inland is this course's territory, and each hop has a distinct blast radius — the number of users a failure at that hop affects.
DNS resolution, TLS negotiation, and load-balancing algorithms are covered there and deliberately not re-derived here. The one thing worth restating because it bites in this module's territory: DNS changes propagate on TTL, so DNS is not a failover mechanism you can rely on within an incident's timeframe. Failover between targets happens at the load balancer, in seconds; failover at DNS happens in minutes to hours and only for clients that honor the TTL.
Region choice answers to three masters and they do not agree. Latency: put compute near users, since physics sets a floor no engineering removes — a round trip from Sydney to Virginia is over 200 ms before your code runs. Price: the same instance costs meaningfully more in some regions than others, and egress pricing varies too. Data residency: where the data may lawfully sit.
The third one is not an engineering input. It is a constraint that selects the region, and engineering optimizes within what is left. GDPR-scoped personal data, sector rules on health and financial records, contractual commitments in enterprise agreements, and public-sector procurement terms all operate this way: they decide, and everything else negotiates around them.
Multi-region is where teams underestimate by an order of magnitude, because it sounds like multi-AZ with bigger numbers. It is not. Synchronous replication across 80 ms is unusable for write paths, so you are choosing between asynchronous replication (accepting data loss on failover, quantified as an RPO you must state) and a multi-primary design (accepting conflict resolution as a permanent feature of your data model). Add cross-region data transfer costs, duplicated infrastructure, and a testing burden that most teams never actually discharge. Multi-AZ is a configuration decision. Multi-region is an architecture, a budget line, and a standing operational commitment.
You have seen the retrofit from the compliance side. A product launches in one region because that is where the team was; two years later an enterprise contract or a regulator requires EU customer data to stay in the EU, and the discovery begins: the primary database, the object storage holding label images, the search index, the analytics warehouse, the log aggregation, the backups, and the third-party processors — each independently placed, each needing to move, several with no clean migration path and some with vendors that cannot offer the region at all. This is a program of work measured in quarters, and it is the same lesson from a different angle: residency is decided at the beginning, by counsel, and written down. "Which region?" looks like a dropdown and is a legal determination.
Every module since the second has quietly assumed something: that compute is replaceable. Kubernetes replaces pods, serverless recycles instances, the PaaS discards a filesystem on every deploy. That assumption has a precondition almost nobody states, and this module states it: compute can only be disposable if it holds nothing unique.
So this module is two subjects that are really one. Scaling — vertical versus horizontal, and the mechanics of autoscaling — and state, which is what horizontal scaling demands you move. The wine critic's newsletter is coming, and forty times normal traffic in two minutes will find every place cellarbook cheated.
Vertical scaling means a bigger box. It is gloriously simple: change the instance size, restart, done — no code changes, no distributed-systems reasoning, no new failure modes. Its limits are equally clear. There is a maximum instance size, so the ceiling is hard; the resize needs a restart, so it costs a maintenance window; the price curve steepens at the top end, where the largest instances cost disproportionately more per unit; and one big box is one failure domain no matter how big it is.
Horizontal scaling means more boxes behind a load balancer. There is no practical ceiling, capacity can move in both directions continuously, and the redundancy is free because the boxes were already there. It costs you a balancer, a deployment story that handles N instances, and one precondition that is not optional and not negotiable: every instance must be able to serve every request.
The two also apply asymmetrically to the two halves of a system, which is the practical detail. Stateless application servers scale horizontally with no drama. Databases do not: read traffic scales out well via replicas, and write traffic ultimately concentrates on one primary, so a write-bound database is the one component where vertical scaling remains the correct first answer for a long time. That asymmetry is why the reference architecture in every module of this course looks the same — a horizontally scaled stateless tier in front of a vertically scaled stateful one.
Statelessness means no instance holds anything a request might need that other instances lack. If any instance holds unique state, the load balancer must send the right requests to the right instance — which it fundamentally does not know how to do — and removing an instance destroys data.
Cellarbook scaled from one instance to three and broke immediately, in two places, both textbook:
Sessions in process memory. The Flask app kept the session dictionary in a module-level structure. With one instance, every request hit the same memory. With three, a user logs in against instance A and their next request lands on instance B, which has no record of them and returns them to the login page. Users are logged out apparently at random, at a rate that rises with instance count — one instance in three serves you correctly, so two of three requests fail.
Label images on local disk. Uploads landed on whichever instance handled the POST. A subsequent request for that image succeeds only if it lands on the same instance — a one-in-three coin flip — so images 404 intermittently for their owners. Then the traffic spike passes, the group scales in, and one instance is terminated: the images on it are gone permanently, with no error, no alert, and no backup, because nothing in the system ever treated that disk as a place data lived.
Symptom: failures that correlate with instance count — intermittent logouts, files that 404 for some users, caches that behave differently across deploys. Anything that gets worse when you add capacity is this. Corrective: externalize. And note the sequencing trap: these bugs are invisible on one instance, so the app appears correct right up until the moment you need it to scale, which is the moment you least want to be doing surgery.
The corrective is one move applied three ways: state leaves the compute and goes somewhere durable and shared. Compute becomes disposable precisely because state became someone else's problem — and "someone else" here means a purpose-built service that replicates, backs up, and survives.
| State | Wrong home | Right home | Why |
|---|---|---|---|
| Sessions | process memory | shared session store, or signed stateless tokens | any instance can validate any user's session |
| Uploaded files | local disk | object storage | durable, multi-AZ, effectively unbounded, served directly or via CDN |
| Application data | SQLite on the instance | managed database | replication, backups, and failover as a product, not a project |
| Caches | per-process dictionaries | shared cache — or accept per-instance caches deliberately | per-instance caches are fine if the app is correct when they are empty and inconsistent |
| Background jobs | in-process threads | a queue plus workers | work survives the death of the instance that accepted it |
Guide Nº 06 covers what happens once data reaches the managed database — modeling, transactions, consistency — and is not re-derived here. Guide Nº 14 tells the sticky-sessions story from the load balancer's side, and this module's telling agrees with it: session affinity is a crutch, an externalized session store is the fix.
Autoscaling is a feedback loop: measure a signal, compare to a target, add or remove capacity. Its behavior is dominated not by the policy but by the delay between the load arriving and the capacity being useful, and that delay is a sum of four stages that are usually estimated as one.
Metric collection — the signal is published on an interval, so the loop is already looking at the past (15–60 s). Evaluation — most policies require the condition to hold across consecutive periods before acting, because a policy that reacts to one sample thrashes (30–120 s). Provisioning — the instance is requested, allocated, and booted (30–90 s for a VM, seconds for a pod on an existing node, minutes if a new node is needed too). Warm-up — the process starts, connections open, caches fill, and health checks pass before traffic arrives (10–60 s). Total: commonly two to five minutes from load arriving to capacity serving.
Now the newsletter. A wine critic links a cellar at 09:00:00 and traffic reaches 40× normal within two minutes. The loop cannot win this race — not because it is badly tuned, but because the spike's rise time is shorter than the sum of its stages. Users during the window get timeouts and errors; capacity arrives after the peak has passed. Autoscaling is a shock absorber for gradual load change and a bystander for step functions.
Autoscaling is a control loop with lag, so it handles load that changes more slowly than the loop's total delay. For known events — a newsletter, a launch, a sale, a scheduled batch — pre-scale on the calendar and let the loop handle the drift around your estimate. Using autoscaling as a substitute for understanding your load is how teams discover, during the incident, that they bought a shock absorber and needed a schedule.
Module 6 gave you the spectrum and deliberately withheld half of it. A responsibility grid tells you who does the work; it does not tell you what the work costs, and "choose by what you want to stop managing" only becomes a decision when the price of stopping is on the table. This module puts it there.
The argument is that each position's bill has a shape that follows directly from what the provider is holding for you, so the spectrum predicts the invoice. Then the four cellarbook deployments land side by side — at today's traffic and at ten times it — with a fourth column nobody prices and everybody pays: who gets paged. The course closes with a decision framework, the anti-pattern gallery, and a way of reading an architecture diagram that you already know from another profession.
Four positions, four pricing logics, and each one follows from what the provider is holding.
Residency (VMs). You pay for a machine to exist, per hour, whether it serves a million requests or none. The bill is flat, predictable, and completely indifferent to your traffic — which is a virtue when you are forecasting and a waste when you are idle. You are renting a thing that sits there.
Capacity (Kubernetes). You pay for the nodes plus the control plane. The control plane is a floor that never drops — an empty cluster costs the same $75/month (illustrative) as a busy one — and the nodes are residency pricing with an orchestration layer above them. Total cost has a high fixed component and a shallow slope, so unit costs fall as you fill the cluster and the entry price is brutal for small workloads. You are renting room to place things.
Usage (serverless). You pay per invocation and per millisecond of execution. Zero traffic is zero compute cost. Sustained traffic bills every millisecond of every request at a rate that is high per unit because the provider is absorbing your capacity risk. Steep slope, no floor. You are renting work performed.
Convenience (PaaS). You pay a premium over the compute underneath — roughly $110 for what would be $50 of raw instances — in exchange for the build system, the patching, the deploys, and the operational surface you never see. Residency pricing with a markup, and the markup is the product. You are renting someone else's operations team, thinly sliced.
This is why module 6's grid predicts the invoice. Each cell you hand across the frontier is work the provider now performs and prices — and prices with a margin, because they are a business. Moving right almost always increases the per-unit cost of compute and decreases the total cost of operations. Whether that trade is good depends entirely on how expensive your operations are, which for a two-person team is very.
The same instance has three prices, and the difference between them is a commitment you make about time or about interruption.
On-demand is the flexibility premium: full price, start and stop whenever, no commitment. Correct for anything unpredictable, short-lived, or new — and correct for your entire footprint until you know what your steady state is, because a commitment made on a guess is worse than the premium.
Reserved (or committed-use) trades a promise of residency — typically one or three years — for a discount, commonly 30–60%. You are telling the provider they can plan capacity around you, and they price that certainty. Correct for the floor of your usage, the part that will exist in eighteen months regardless: the baseline API instances, the database. The trap is over-committing to a shape you outgrow, since a reservation for an instance family you have migrated away from is a bill for nothing.
Spot is spare capacity at a deep discount — often 60–90% off — with the provider's right to reclaim it on short notice, sometimes as little as two minutes. Correct for work that is interruptible and restartable: batch processing, CI runners, media encoding, stateless workers pulling from a queue. Incorrect, categorically, for anything that cannot be killed mid-operation without consequence.
For cellarbook: the API baseline on reserved (it will exist next year), spike capacity on-demand (unpredictable by definition), the label-image workers on spot (a killed job returns to the queue and reruns; the only cost is latency), and the Postgres primary on on-demand or reserved and never on spot.
Guide Nº 11 covers unit economics: cost per active user, gross margin, and how infrastructure spend behaves as a product scales. The connection to make here is that the bill shapes in this module determine how your unit costs move with growth. A usage-shaped bill keeps cost-per-user roughly constant, which is predictable and never improves; a capacity-shaped bill has a high fixed component that amortizes, so cost-per-user falls as you grow. Which shape you want depends on where you are on the curve — and choosing the shape that suits the company you are today is how you avoid subsidizing the company you might become.
Here is the exhibit, complete. All figures illustrative and order-of-magnitude.
| V1 — one VM | V2 — managed k8s | V3 — serverless | V4 — PaaS | |
|---|---|---|---|---|
| Today (~2,000 daily users) | ~$50 | ~$270 | ~$25 | ~$110 |
| At 10× traffic | ~$150, then a wall | ~$400 | ~$180 | ~$350 |
| Bill shape | residency | capacity — floor never drops | usage — zero when idle | convenience — residency + markup |
| Dominant cost | the instance | control plane + nodes | the database, until traffic grows | the platform premium |
| Paged for | everything, incl. the kernel | everything on the cluster | code, config, and the bill | the app |
The fourth column nobody prices. V1 pages you for the kernel, the OS, the database, and the application — every layer, one person. V2 pages the provider for the control plane and node hardware, and you for everything running on it, which at two thousand daily users means you are paying $270 a month to be paged for roughly the same things as V1 plus a cluster. V3 pages the provider for everything below your handler and you for code, configuration, and the bill — a genuinely small rotation, provided you accept that when the platform has an incident you have no move except to wait and communicate. V4 pages the provider for the platform and you for the app. If you price an engineer's interrupted evening at anything above zero, this column dominates the table for a small team, and it is the column that appears on no invoice.
Five questions, in order. The order matters: each one narrows the field before the next is asked, and a team that answers them honestly rarely disagrees about the conclusion.
Now the gallery. Each of these is a real pattern with a recognizable symptom and a specific corrective.
Résumé-driven Kubernetes. Symptom: a cluster whose replica counts have never changed and whose adoption memo argues from readiness rather than from a problem anyone has had. Corrective: state the deleted toil in hours per month before adopting; if you cannot, run a PaaS until placement, replacement, or routing is a problem you actually have.
Pet containers. Symptom: a fix applied by exec-ing into a running container that evaporates at the next deploy, and a bug that "keeps coming back." Corrective: change the artifact and redeploy; run read-only root filesystems so the wrong move fails loudly.
State on disposable compute. Symptom: failures that get worse when you add capacity — intermittent logouts, files 404ing for some users, data gone after scale-in. Corrective: externalize sessions, files, and data; audit for schedulers and connection pools too, which nobody remembers to look for.
Cold starts ignored until the demo. Symptom: excellent p50, terrible p99, and the stall always hits the first click after a quiet period. Corrective: measure the cold path specifically, trim the package and defer heavy imports, and treat keep-warm pings as evidence the workload wants a resident process.
Autoscaling as a substitute for understanding load. Symptom: a policy tuned repeatedly after each incident, and a spike that outruns the loop every time. Corrective: do the lag arithmetic, pre-scale for calendar-known events, and let the loop absorb drift.
Multi-cloud for portability before product-market fit. Symptom: an architecture restricted to the lowest common denominator of two providers, two sets of expertise to maintain, and no negotiating leverage because you are small on both. Corrective: pick one, use its managed services fully, and keep the coupling documented so a future migration is a scoped project rather than a surprise. Buy the hedge when you are large enough for it to pay.
Lift-and-shift billed as cloud migration. Symptom: a VM copied unchanged into the cloud, a higher bill a year later, and identical reliability. Corrective: be explicit that lift-and-shift buys exit from a data-center lease and nothing else; the cost and reliability improvements come from the re-architecture afterward, which must be funded as its own phase or it will not happen.
Reading a diagram without asking cost or ownership. Symptom: a box everyone recognizes and nobody owns. Corrective: the three questions in the next section.
The promise made in the first module was that an architecture diagram would stop being wallpaper. Here is the method, and you already own it.
Reading a contract, you do not skim for the gist. You take each clause and ask what it does, who it binds, what it displaced in negotiation, and what it costs the client if it is invoked. You do this clause by clause, and the clause everybody skips is where the exposure lives. An architecture diagram is a contract between a team and its infrastructure, and it rewards the same treatment. Three questions, per box:
Apply this to any diagram and the boxes sort themselves into three piles: boxes with three good answers, boxes with a gap you can close in a week, and boxes with no answers at all. The third pile is the finding. In every architecture review, there is at least one box that everyone recognizes, nobody owns, and no one can price — a cache someone added during an incident two years ago, a queue between two services that a departed engineer introduced, a scheduled task on an instance outside the deployment pipeline. It is running. It is billed. It has no owner, no runbook, and no place in anyone's mental model until the morning it stops.
Every box is a choice with a provenance, an owner, and a price. There is no most-modern architecture — only positions on a responsibility spectrum, chosen well or chosen by fashion. You can now interrogate any of them, which was the whole point: not to know which technology is best, but to be the person in the room who asks what each one costs and who is on the hook when it dies.
This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.