feat(root): author networking and configuration curriculum

root-07-m03-services-and-service-discovery — a new, full gold-standard-
pattern module: the Service/Endpoints/DNS request path, choosing the
right Service type. Lesson, demonstration (a Service with zero
Endpoints — the single fastest diagnostic check for this failure
class), knowledge check, guided exercise (ClusterIP vs. NodePort vs.
LoadBalancer for three real components), a real Lab
(expose-service-basics), and a reflection.

root-07-m04-ingress-networkpolicy-foundations — a new PARTIAL module:
Ingress and NetworkPolicy at the architectural level (the security-
control framing of NetworkPolicy is deliberately deferred to ROOT 09).
Lesson + knowledge check + real Labs (ingress-task, network-policy).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 09:06:19 -05:00
co-authored by Claude Sonnet 5
parent 86ea035e9b
commit dbcc210889
8 changed files with 481 additions and 0 deletions
@@ -0,0 +1,87 @@
# The Service/Endpoints/DNS Request Path
## The problem: Pod IPs are not stable
Every Pod gets its own IP address, but that IP is NOT durable — when a Pod is replaced (a
rollout, a crash recovery, a reschedule), the new Pod gets a brand new IP. If another part of
your application had that old IP hardcoded, it would silently start talking to nothing. This is
the exact problem in `expose-service-basics`'s scenario: "other pods cannot reliably connect
because pod IPs change when they restart."
## A Service: a stable address in front of a moving set of Pods
A Service is a stable virtual IP and DNS name that automatically routes to whichever Pods
currently match its label selector — regardless of how many there are or how often they change.
You never talk to a Pod's IP directly in normal operation; you talk to the Service, and the
Service's Endpoints (a live, continuously-updated list of the matching Pods' IPs) determines
where traffic actually goes.
```yaml
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
selector:
app: redis # <- matches Pods by label, same mechanism from ROOT 05
ports:
- port: 6379
```
This is the same label-selector mechanism you learned in ROOT 05 — a Service doesn't know or
care about specific Pod names, only which Pods currently carry a matching label.
## The three Service types you'll use most
- **ClusterIP** (the default) — reachable only from inside the cluster. Use this for
internal-only communication, like one microservice talking to another or to a database.
- **NodePort** — additionally exposes the Service on a static port on every node's own IP,
reachable from outside the cluster. Mostly used for development/testing or when there's no
cloud load balancer available.
- **LoadBalancer** — asks the cloud provider to provision a real external load balancer pointed
at the Service. This is the standard way to expose something to the public internet on a
cloud-hosted cluster.
Choosing wrong isn't just a style preference — a ClusterIP Service is architecturally
unreachable from outside the cluster no matter how correctly everything else is configured.
## DNS: how Pods actually find a Service by name
Every Service automatically gets a DNS name inside the cluster
(`<service-name>.<namespace>.svc.cluster.local`, or usually just `<service-name>` from within
the same Namespace) — this is why application code can use a plain hostname like `redis-svc`
instead of an IP address at all. Kubernetes' internal DNS resolves that name to the Service's
stable virtual IP, which then routes to whichever Pods are currently healthy.
## The full request path, end to end
```
Client -> DNS lookup "redis-svc" -> Service's stable virtual IP
-> Service consults its Endpoints (live list of matching, ready Pods)
-> traffic forwarded to one of those Pod IPs
```
Every one of these steps can fail independently, which is exactly why "the app can't reach
redis" requires checking each layer rather than guessing — the next activity shows you what
that looks like when the Endpoints layer is empty.
## Instructor Notes
**Approximate timing:** 15 minutes read, 10-12 minutes live with the request-path diagram.
**Demo plan:** Before the next activity, it's worth live-running `kubectl get endpoints
<service-name>` against a healthy Service so students see a populated Endpoints list before
they see an empty one in the demonstration.
**Likely misconceptions to watch for:**
- Students often think a Service "is" a load balancer process running somewhere. It's closer to
a routing rule maintained by `kube-proxy` on every node — worth clarifying that there's no
single "Service Pod" to go find and debug.
- ClusterIP vs. NodePort vs. LoadBalancer is sometimes memorized without the "why" — the
knowledge check's Service-type question is designed to test the underlying reasoning, not
just recall.
**Discussion question:** "If a Service's selector matches zero Pods, does the Service itself
fail to exist?" (Answer: no — the Service object exists fine; its Endpoints list is just empty,
which looks like connection failure from the client's side without any error at the Service
level itself. This is exactly the next activity's topic.)
@@ -0,0 +1,82 @@
# Demonstration: A Service With Zero Endpoints
This is one of the most common "it just doesn't work" incidents in real Kubernetes operation —
and it produces no error message at all at the Service level.
## The setup
A Service `api-svc` selects Pods labeled `app: api`. A Deployment was recently updated and its
Pods now carry the label `app: api-v2` instead (a rename during a refactor, deliberately or
accidentally).
## What you'd see from the client side
```
$ curl http://api-svc:8080/health
curl: (7) Failed to connect to api-svc port 8080: Connection refused
```
Just a generic connection failure — nothing here says "your Service's selector doesn't match
any Pods."
## What the Service object itself shows
```
$ kubectl get service api-svc
NAME TYPE CLUSTER-IP PORT(S) AGE
api-svc ClusterIP 10.96.34.201 8080/TCP 2h
```
Completely normal-looking. The Service exists, has a stable IP, is not in any kind of "error"
state — because from the Service's own perspective, nothing is wrong. It's doing exactly what
it's configured to do: route to whatever matches its selector, which currently happens to be
nothing.
## Where the real evidence is: Endpoints
```
$ kubectl get endpoints api-svc
NAME ENDPOINTS AGE
api-svc <none> 2h
```
This is the actual signal. `<none>` means the Service's selector currently matches zero Pods —
this is the single fastest check for "is this a Service-to-Pod mismatch" any time something
reachable-in-principle looks like a connection failure.
## Confirming the mismatch
```
$ kubectl get pods --show-labels
NAME READY STATUS LABELS
api-abc123 1/1 Running app=api-v2
```
The Pod is healthy and running — `app=api-v2`, not `app=api`. The Service's selector
(`app: api`) matches nothing, and nothing about the Pod itself is broken.
## The general diagnostic shape
`curl`/application-level connection failure tells you *that* something's wrong, but nothing
about *where*. `kubectl get endpoints <service>` is the fastest way to distinguish "the Service
has no matching Pods" from "the Pods exist and are ready but something else is wrong" —
reach for it early whenever a Service-fronted connection fails for no obvious reason.
## Instructor Notes
**Demo script:** Live-create this exact mismatch (a Service with one selector, a Deployment
with a slightly different label) and walk through `kubectl get service` -> `kubectl get
endpoints` -> `kubectl get pods --show-labels` in front of the class, narrating what each
command is and isn't telling you.
**Approximate timing:** 8-10 minutes live; 5-7 minutes self-paced.
**Common misconception to preempt:** students often check the Service object first and, seeing
nothing obviously wrong, assume the problem must be elsewhere entirely (DNS, firewall, the
application itself) — reinforce that `kubectl get endpoints` is the specific, fast check for
this exact failure mode, before reaching for anything more complex.
**Optional extension:** ask what `kubectl describe service api-svc` would additionally show
compared to `kubectl get service` (answer: the same selector the Service is using, which lets
you directly compare it against `kubectl get pods --show-labels`'s actual Pod labels side by
side, rather than inferring the mismatch).
@@ -0,0 +1,47 @@
# Knowledge Check: Services & Service Discovery
---
**1. A Deployment has three replicas but `kubectl get endpoints` shows `<none>` for its Service.
Which part of the system should you inspect first, and why?**
*Answer:* Compare the Service's selector (`kubectl describe service`) against the actual Pods'
labels (`kubectl get pods --show-labels`) — a Service with a healthy backend Deployment but zero
Endpoints almost always means the selector doesn't match the Pods' current labels, not that the
Pods themselves are unhealthy.
---
**2. You need a microservice reachable ONLY from other services inside the same cluster, never
from the public internet. Which Service type, and why not LoadBalancer?**
*Answer:* ClusterIP — it's reachable only from inside the cluster by design, which is exactly
the requirement. A LoadBalancer would provision a real external endpoint reachable from the
public internet, which is more exposure than the requirement calls for, and depends on cloud
provider support this scenario doesn't need.
---
**3. True or false: application code that connects to `redis-svc` by that plain hostname is
relying on Kubernetes' internal DNS to resolve it.**
*Answer:* True. Every Service automatically gets a DNS name; that's specifically what lets code
use a stable hostname instead of hardcoding a Pod IP that could change at any time.
---
**4. A Service's Endpoints list is non-empty, but the application still can't connect. Does
this rule out a Service-level problem entirely?**
*Answer:* Not entirely — a non-empty Endpoints list means at least one Pod currently matches the
selector, but the connection could still fail for other reasons (wrong port number on the
Service, a NetworkPolicy blocking the traffic, the application inside the Pod not actually
listening). A populated Endpoints list narrows the problem, it doesn't prove the whole path
works.
## Instructor Notes
**Facilitation:** Question 1 is the single most transferable diagnostic habit from this module —
worth explicitly calling out as "the first thing to check" for this entire class of problem.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,49 @@
# Guided Exercise: Choosing the Right Service Type
Paper-and-thinking exercise before the Lab.
## The scenario
You're deploying three components for a class project on a cloud-hosted cluster:
1. A Postgres database that only your own backend application should ever talk to.
2. A backend API that the frontend (also running in the cluster) calls internally.
3. The frontend web application itself, which needs to be reachable by users on the public
internet.
## Your task
For each of the three components, decide: ClusterIP, NodePort, or LoadBalancer — and justify
each choice in one sentence referencing WHO needs to reach it.
## A worked answer to check yourself against
1. **Postgres — ClusterIP.** Only your own backend (running inside the same cluster) needs to
reach it; there is no reason for it to be reachable from outside the cluster at all, and
exposing a database externally is a real, avoidable security risk.
2. **Backend API — ClusterIP.** The frontend calls it from inside the cluster too — same
reasoning as the database. External users never talk to the backend directly; they only ever
go through the frontend.
3. **Frontend — LoadBalancer** (on a real cloud-hosted cluster). Public internet users need to
reach it, and a cloud provider's LoadBalancer is the standard mechanism for that. NodePort
would technically work but is typically used for development/testing rather than a real
public-facing service on a cloud cluster.
## Self-check
If you chose LoadBalancer for the database or the backend API, reconsider — "internet-reachable
by default" is not the safe default for anything that doesn't need to be, and getting this
choice right the first time avoids a real security exposure later, not just a style issue.
## Instructor Notes
**Facilitation:** Push on the "why not just LoadBalancer everything, it works" instinct some
students will have — this is a good early moment to connect Service-type choice to the security
mindset that ROOT 09 builds on more fully (exposure should be intentional, not the path of
least resistance).
**Approximate timing:** 10 minutes live with discussion; 10 minutes self-paced.
**Optional extension:** ask what would need to be added (not covered in this module) to let
public users reach the frontend at a real domain name over HTTPS rather than a bare
LoadBalancer IP — a natural forward pointer to Ingress, covered in the next module.
@@ -0,0 +1,40 @@
# Launch Lab: Exposing an Application
Time to create a real Service in your disposable Lab and connect it to a real Deployment.
## What you're doing
A `redis` Deployment is running, but other Pods can't reliably reach it because Pod IPs change
whenever a Pod restarts — exactly the problem the lesson opened with. Your task is to create a
stable ClusterIP Service in front of it.
## Before you start
Recall the request path from the lesson: your Service's `selector` has to match the Deployment
Pods' actual labels, or you'll end up with the empty-Endpoints situation from the
demonstration. Check the Deployment's Pod labels before writing your Service's selector, don't
guess at them.
## Your task in this Lab
Create a ClusterIP Service exposing the `redis` Deployment on the correct port. Once it's
created, verify it the same way the demonstration did: `kubectl get service`, then `kubectl get
endpoints` to confirm it actually has a non-empty Endpoints list, not just that the Service
object exists.
This Lab has its own in-context hints if you get stuck.
When you're done, return to this module for a reflection on why service discovery is
infrastructure, not magic.
## Instructor Notes
**Classroom flow:** Encourage students to run `kubectl get endpoints` as their OWN verification
step even though the Lab's automated validation will check this too — building the habit of
checking Endpoints, not just "the Service exists," is the actual transferable skill here.
**Approximate timing:** 10-15 minutes.
**Common mistake to watch for:** a selector that matches on the wrong label key/value, or a
port/targetPort mismatch (the Service's `port` is what clients connect to; `targetPort` is what
the container actually listens on — conflating the two is a very common first-time error).
@@ -0,0 +1,40 @@
# Reflection: Service Discovery Is Infrastructure, Not Magic
You just created a Service that gives a moving set of Pods a stable address. Take a few minutes
to connect this to real operational thinking.
## Reflection questions
1. Before this module, "the app can just connect to redis by name" might have seemed obvious or
automatic. What specific mechanism actually makes that true, and what would break it?
2. If your Service's selector had been wrong (matching zero Pods), what would the SYMPTOM have
looked like from the application's side, versus what the actual CAUSE would have been?
3. Why is checking `kubectl get endpoints` before assuming a networking/DNS/firewall problem a
good default habit, given how common a selector mismatch actually is?
## Real Ops takeaway
**"It's probably a networking issue" is often wrong, and Endpoints is how you find out fast.** A
huge fraction of real "service X can't reach service Y" incidents in Kubernetes are Service
selector mismatches, not actual network problems — checking `kubectl get endpoints` first, before
escalating to firewall rules or DNS debugging, saves real time precisely because this failure
mode produces zero errors at the layers people instinctively check first.
**Stable addressing is what makes rolling updates and self-healing actually usable.** Everything
you learned in ROOT 06 about Pods being replaced during a rollout or after a crash only works
smoothly in practice because Services give callers a stable target that survives those
replacements — without this layer, every Pod replacement would require updating every caller's
configuration by hand.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** use question 2 as a discussion prompt — the
symptom/cause gap is exactly why this class of bug is often under-diagnosed in real teams.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Optional extension:** ask students to write the one-line runbook entry they'd want a future
on-call engineer to have for "Service X seems unreachable" — a good exercise in turning a
lesson into something operationally reusable.
@@ -0,0 +1,90 @@
# Ingress and NetworkPolicy: What They Architecturally Do
## Ingress: routing external HTTP(S) traffic by host/path
You already know a LoadBalancer Service can expose something to the public internet. But a real
application usually needs more than "one raw IP to one Service" — you might want
`api.example.com` and `app.example.com` to route to different backend Services, all through one
entry point, with TLS termination handled in one place instead of per-Service.
**Ingress** is the Kubernetes object for exactly this: a set of HTTP routing rules (by hostname
and/or URL path) that an Ingress controller reads and turns into actual load-balancer/proxy
configuration.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-svc
port: { number: 80 }
```
An important distinction: Ingress is a set of ROUTING RULES, not a load balancer itself — an
Ingress controller (a separate piece of software running in the cluster) is what actually reads
these rules and does the routing. Without a controller installed, an Ingress object exists in
the API but nothing acts on it.
## NetworkPolicy: traffic reachability as something you declare, not assume
By default, every Pod in a Kubernetes cluster can reach every other Pod — there is no built-in
network isolation between them at all. This surprises people coming from a "obviously the
network is locked down unless configured otherwise" assumption. It is exactly backwards from
that assumption: **everything is reachable until you say otherwise.**
A NetworkPolicy changes this for the Pods it selects, by declaring which traffic IS allowed —
everything not explicitly allowed to a Pod covered by at least one NetworkPolicy is then denied.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
spec:
podSelector:
matchLabels: { app: backend }
ingress:
- from:
- podSelector:
matchLabels: { app: frontend }
```
This example allows traffic to `backend` Pods only from `frontend` Pods — once this policy
exists and selects `backend`, every OTHER Pod trying to reach `backend` is now blocked, even
though nothing was true about that before the policy existed.
## Why this matters architecturally, before it matters as a security control
Right now, think of NetworkPolicy purely as "declaring intended reachability, not assuming it."
ROOT 09 comes back to NetworkPolicy specifically as a SECURITY control (default-deny postures,
least-privilege network access) — this module is deliberately just the architectural mental
model: Kubernetes doesn't isolate network traffic for you by default, and NetworkPolicy is the
mechanism for declaring the reachability you actually intend.
## Instructor Notes
**Approximate timing:** 15 minutes read; 10 minutes live.
**Likely misconceptions to watch for:**
- Students often assume Ingress by itself "does" the routing. Emphasize the controller
requirement — an Ingress object with no controller installed does nothing.
- The "everything reachable by default" fact genuinely surprises most students the first time
they hear it — it's worth stating plainly and letting it land rather than rushing past it.
**Discussion question:** "If NO NetworkPolicy exists anywhere in a Namespace, what's the actual
reachability between Pods in that Namespace?" (Answer: completely open — every Pod can reach
every other Pod, matching the "everything reachable by default" rule.)
**Optional extension:** ask what would happen if you created a NetworkPolicy that selects
`frontend` Pods but declares no `ingress` rules at all (answer: `frontend` Pods would become
completely unreachable by anything — an empty ingress list inside a NetworkPolicy that HAS an
ingress policyType means "deny all incoming," not "no policy applies").
@@ -0,0 +1,46 @@
# Knowledge Check: Ingress & NetworkPolicy Foundations
---
**1. You create an Ingress object with correct routing rules, but no traffic is ever routed
according to them. What's the most likely reason?**
*Answer:* No Ingress controller is installed/running in the cluster. Ingress objects are just
routing rules; a separate controller has to actually read and act on them, or nothing happens.
---
**2. True or false: by default, a Pod in Namespace A cannot reach a Pod in Namespace B unless a
NetworkPolicy explicitly allows it.**
*Answer:* False — the opposite is true by default. With NO NetworkPolicy in place at all,
every Pod can reach every other Pod regardless of Namespace. NetworkPolicy is what RESTRICTS
this default-open behavior, not what's required to enable connectivity in the first place.
---
**3. A NetworkPolicy selects Pods labeled `app: backend` and declares one ingress rule allowing
traffic from Pods labeled `app: frontend`. What happens to traffic from a Pod labeled
`app: admin-tool` trying to reach `backend`?**
*Answer:* It's blocked. Once a NetworkPolicy selects a Pod, only traffic matching one of its
explicit rules is allowed — everything else to that Pod is denied, including traffic that would
have worked fine before the policy existed.
---
**4. Why is Ingress the right tool for exposing an HTTP application by hostname, rather than
creating a separate LoadBalancer Service per hostname?**
*Answer:* A LoadBalancer Service typically provisions its own external IP/load balancer per
Service — one per hostname would mean paying for and managing N separate external endpoints.
Ingress lets one entry point route to many backend Services by hostname/path, which is both
cheaper and centralizes concerns like TLS termination.
## Instructor Notes
**Facilitation:** Question 2 catches a very common and consequential misconception — worth
confirming out loud that everyone actually updated their mental model, not just guessed
correctly.
**Approximate timing:** 5 minutes.