Add lessons: untimed guided labs, one per curriculum objective, on a host of their own, in the browser and in the terminal. Each step has a check, a hint and its commands; a lesson writes no history or scores, only a done mark on its objective. CKAD 2.1-2.4 (24 steps) and all 27 CKA objectives (135 steps) have one, and `./ksim lesson audit` proves that every step fails, is done by its own commands, then passes. Add CKA tasks 132-147 (cka/bank_course.py) on what a full CKA course teaches beyond the bank: a real upgrade from v1.36, a drain under a PodDisruptionBudget, QoS and eviction, in-place resize, kube-proxy's chains, encryption at rest, an operator, and CSI expansion, snapshots and clones (ensure_csi_hostpath). Move CKA and CKS to Kubernetes v1.37 and rebuild the hosts on Ubuntu 24.04, with kubelet, kubeadm and kubectl as held packages from pkgs.k8s.io; the desktop runs Ubuntu too. Hosts gain man pages, hexdump and xxd. Fix CKS Q4, which gave 3 of 12 points untouched, and CKA Q4 and Q49, whose solutions raced their new CRD. Audited on v1.37: CKA 147/147, CKAD 105/105, CKS 109/110 (Q103, a Falco task, failed to seed once and awaits a re-audit); lessons: 135 of 135 CKA steps and 24 of 24 CKAD steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
677 lines
42 KiB
Python
677 lines
42 KiB
Python
"""CKA lessons of the Services and Networking domain (5.1-5.6). See cka/lessons.py for what a lesson is."""
|
|
from cka.common import DNS
|
|
from sim.bank import NET, NS, PODS, step
|
|
|
|
# ---- 5.1 connectivity between Pods ------------------------------------------------------------
|
|
L51 = {
|
|
"id": 9501, "objective": "5.1", "workers": 2,
|
|
"title": "The Pod network: addresses, routes between nodes, and the host network",
|
|
"intro": r"""Kubernetes asks three things of a network, and leaves the rest to the CNI plugin:
|
|
|
|
```
|
|
every Pod has its own IP from the node's slice of the Pod CIDR (node.spec.podCIDR)
|
|
every Pod reaches every other Pod across nodes, without NAT
|
|
agents on a node reach its Pods the kubelet, for probes
|
|
```
|
|
|
|
How the packets get from one node to another is the plugin's business: kindnet, here, adds a route on each node for every other node's Pod CIDR, via that node's address. Others use overlays (VXLAN) or BGP.
|
|
|
|
A Pod with `hostNetwork: true` gets no IP of its own - it uses the node's network namespace and address.
|
|
|
|
Namespace `net` has Pod `a` on `{W1}` and Pod `b` on `{W2}`; each serves its own name on port 80.""",
|
|
"setup": PODS + r"""
|
|
mkns net
|
|
mkdir -p "$WS/lesson-5.1"
|
|
for p in a:$W1 b:$W2; do
|
|
$K -n net run "${p%%:*}" --image=busybox:1.36 --overrides="{\"spec\":{\"nodeName\":\"${p#*:}\"}}" --command -- \
|
|
sh -c 'mkdir -p /www && echo "$HOSTNAME" > /www/index.html && exec httpd -f -p 80 -h /www' >/dev/null
|
|
done
|
|
ready net || exit 1
|
|
""",
|
|
"steps": [
|
|
step("Each node's share of the Pod network",
|
|
r"""Write each node's Pod CIDR to `{WS}/lesson-5.1/cidrs.txt`, one line per node: `NODE CIDR`. Then compare with the Pod IPs in `kubectl get pods -A -o wide`: every Pod's address is from its node's range.""",
|
|
check=r"""
|
|
want=$($K get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.spec.podCIDR}{"\n"}{end}' | sort)
|
|
eq "$(tr -s ' \t' ' ' < "$WS/lesson-5.1/cidrs.txt" 2>/dev/null | tr -d '\r' | sed 's/ *$//' | grep . | sort)" "$want" """,
|
|
hint=r"""`.spec.podCIDR` of each node: `-o jsonpath='{range .items[*]}{.metadata.name} {.spec.podCIDR}{"\n"}{end}'`, or `custom-columns`. The cluster-wide range is the `podSubnet` in kubeadm's configuration.""",
|
|
show=r"""kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.spec.podCIDR}{"\n"}{end}' > {WS}/lesson-5.1/cidrs.txt
|
|
cat {WS}/lesson-5.1/cidrs.txt
|
|
kubectl -n net get pods -o wide"""),
|
|
step("Across nodes, Pod to Pod",
|
|
r"""From Pod `a`, send a request straight to Pod `b`'s **IP address** on port 80 - no Service - and save the answer in `{WS}/lesson-5.1/answer.txt`. It crossed from `{W1}` to `{W2}`.""",
|
|
check=r"""eq "$(tr -d ' \r\n' < "$WS/lesson-5.1/answer.txt" 2>/dev/null)" b""",
|
|
hint=r"""`kubectl -n net get pod b -o jsonpath='{.status.podIP}'`, then `kubectl -n net exec a -- wget -qO- -T3 http://IP`.""",
|
|
show=r"""ip=$(kubectl -n net get pod b -o jsonpath='{.status.podIP}')
|
|
kubectl -n net exec a -- wget -qO- -T3 http://$ip | tee {WS}/lesson-5.1/answer.txt"""),
|
|
step("The route that carries it",
|
|
r"""On `{W1}`, look at the routing table: there is a route for `{W2}`'s Pod CIDR, via `{W2}`'s node address. Write that route's destination and gateway to `{WS}/lesson-5.1/route.txt`, as `CIDR via ADDRESS`.""",
|
|
check=r"""
|
|
c=$(j node "$W2" '{.spec.podCIDR}'); gw=$(j node "$W2" '{.status.addresses[?(@.type=="InternalIP")].address}')
|
|
nx "$W1" "ip route | grep -q '^$c via $gw '" </dev/null && eq "$(tr -s ' ' < "$WS/lesson-5.1/route.txt" 2>/dev/null | tr -d '\r\n' | sed 's/ *$//')" "$c via $gw" """,
|
|
hint=r"""`ip route` on the node. The node's address is its `InternalIP` (`kubectl get nodes -o wide`).""",
|
|
show=r"""ssh -n {W1} ip route
|
|
c=$(kubectl get node {W2} -o jsonpath='{.spec.podCIDR}')
|
|
ssh -n {W1} ip route | grep "^$c " | awk '{print $1, $2, $3}' > {WS}/lesson-5.1/route.txt
|
|
cat {WS}/lesson-5.1/route.txt"""),
|
|
step("A Pod on the host's network",
|
|
r"""Run Pod `hostnet` in `net` (`busybox:1.36`, `sleep 86400`) with `hostNetwork: true`, on `{W1}`. Its IP is not from the Pod CIDR: it is `{W1}`'s own address, and it sees the node's interfaces. Write its IP to `{WS}/lesson-5.1/hostnet.txt`.""",
|
|
check=r"""
|
|
want=$(j node "$W1" '{.status.addresses[?(@.type=="InternalIP")].address}')
|
|
eq "$(j -n net pod hostnet '{.spec.hostNetwork} {.status.podIP}')" "true $want" && eq "$(tr -d ' \r\n' < "$WS/lesson-5.1/hostnet.txt" 2>/dev/null)" "$want" """,
|
|
hint=r"""`spec.hostNetwork: true` and `spec.nodeName: {W1}` (or a nodeSelector). `kubectl run ... --overrides='{"spec":{...}}'` adds both.""",
|
|
show=r"""kubectl -n net run hostnet --image=busybox:1.36 --overrides='{"spec":{"hostNetwork":true,"nodeName":"{W1}"}}' -- sleep 86400
|
|
kubectl -n net wait --for=condition=Ready pod/hostnet --timeout=120s
|
|
kubectl -n net get pod hostnet -o jsonpath='{.status.podIP}' > {WS}/lesson-5.1/hostnet.txt
|
|
kubectl -n net exec hostnet -- ip -4 addr show eth0"""),
|
|
],
|
|
"outro": r"""You read how the Pod network is divided among nodes, sent a request from one node's Pod to another's, found the route that carried it, and ran a Pod on the host network.
|
|
|
|
**For the exam**
|
|
|
|
- Pod IPs: `kubectl get pods -o wide`; the node's range: `.spec.podCIDR`; the cluster's: kubeadm's `podSubnet` (or the CNI's own configuration).
|
|
- Pods reach each other by IP across nodes with no NAT; Services and DNS are on top of that.
|
|
- Which CNI runs: its Pods in `kube-system` (or its own Namespace) and its file in `/etc/cni/net.d/`. NetworkPolicy works only if the plugin enforces it.
|
|
- `hostNetwork: true` Pods share the node's ports: two of them cannot listen on the same port on one node.
|
|
|
|
Next, prove it against the clock: **drill 5.1**.""",
|
|
}
|
|
|
|
# ---- 5.2 NetworkPolicies ----------------------------------------------------------------------
|
|
L52 = {
|
|
"id": 9502, "objective": "5.2", "workers": 1,
|
|
"title": "NetworkPolicies: deny by default, then allow exactly what is needed",
|
|
"intro": r"""Without NetworkPolicies, every Pod accepts traffic from everywhere. A policy **selects** Pods (`podSelector`) and, for those Pods only, allows the traffic its rules list - everything else in that direction is dropped. Policies add up: a packet passes if any policy allows it.
|
|
|
|
```
|
|
podSelector: {} every Pod in the Namespace
|
|
from: [{podSelector}] Pods of THIS Namespace
|
|
from: [{namespaceSelector}] every Pod of the matching Namespaces
|
|
from: [{namespaceSelector, podSelector}] one element: those Pods IN those Namespaces (AND)
|
|
from: [{namespaceSelector}, {podSelector}] two elements: either (OR)
|
|
```
|
|
|
|
An egress policy also blocks DNS unless it allows UDP and TCP 53 to `kube-dns`.
|
|
|
|
Namespace `prod` (label `team=prod`) runs Pods `db`, `api` and `web`, each with a Service of its name on port 80. Namespace `dev` (label `team=dev`) runs `api`, `tester` and `echo`. Nothing is restricted yet.""",
|
|
"setup": PODS + r"""
|
|
mkns prod; mkns dev
|
|
$K label ns prod team=prod --overwrite >/dev/null; $K label ns dev team=dev --overwrite >/dev/null
|
|
for p in db api web; do srv prod $p; done
|
|
for p in api tester echo; do srv dev $p; done
|
|
ready prod && ready dev || exit 1
|
|
""",
|
|
"steps": [
|
|
step("Close the Namespace",
|
|
r"""Create NetworkPolicy `default-deny` in `prod`: it selects every Pod and allows **no** ingress. From now on, nothing reaches the Pods of `prod` - not even from inside `prod`.""",
|
|
check=NET + r"""
|
|
eq "$(j -n prod netpol default-deny '{.spec.policyTypes[0]}')" Ingress && [ -z "$(j -n prod netpol default-deny '{.spec.ingress}')" ] || exit 1
|
|
blocked prod api http://db && blocked dev tester http://web.prod""",
|
|
hint=r"""`podSelector: {}` and `policyTypes: [Ingress]`, with no `ingress:` list. Test with `kubectl -n prod exec api -- wget -qO- -T3 db`.""",
|
|
show=r"""kubectl -n prod apply -f - <<'EOF'
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: default-deny
|
|
spec:
|
|
podSelector: {}
|
|
policyTypes: [Ingress]
|
|
EOF
|
|
kubectl -n prod exec api -- wget -qO- -T3 db || echo blocked"""),
|
|
step("Let the API reach the database",
|
|
r"""Create NetworkPolicy `db-from-api` in `prod`: Pods `app=db` accept TCP 80 from Pods `app=api` **of `prod`**. `web` still cannot reach `db`, and neither can `dev`'s `api` - a `podSelector` alone means "in this Namespace".""",
|
|
check=NET + r"""okr prod api http://db && blocked prod web http://db && blocked dev api http://db.prod""",
|
|
hint=r"""`podSelector: {matchLabels: {app: db}}`, `ingress: [{from: [{podSelector: {matchLabels: {app: api}}}], ports: [{protocol: TCP, port: 80}]}]`.""",
|
|
show=r"""kubectl -n prod apply -f - <<'EOF'
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: db-from-api
|
|
spec:
|
|
podSelector:
|
|
matchLabels: {app: db}
|
|
policyTypes: [Ingress]
|
|
ingress:
|
|
- from:
|
|
- podSelector:
|
|
matchLabels: {app: api}
|
|
ports:
|
|
- {protocol: TCP, port: 80}
|
|
EOF
|
|
kubectl -n prod exec api -- wget -qO- -T3 db
|
|
kubectl -n prod exec web -- wget -qO- -T3 db || echo blocked"""),
|
|
step("Those Pods in that Namespace",
|
|
r"""Create NetworkPolicy `web-from-dev-api` in `prod`: Pods `app=web` accept TCP 80 from Pods `app=api` **in** Namespaces labelled `team=dev` - both conditions in **one** element of `from`.
|
|
|
|
`dev`'s `api` gets in; `dev`'s `tester` does not, and neither does `prod`'s own `api`. Written as two elements, the rule would let every Pod of `dev` in, and every `api` Pod of `prod` too.""",
|
|
check=NET + r"""okr dev api http://web.prod && blocked dev tester http://web.prod && blocked prod api http://web""",
|
|
hint=r"""`from: [{namespaceSelector: {matchLabels: {team: dev}}, podSelector: {matchLabels: {app: api}}}]` - mind the single `-`.""",
|
|
show=r"""kubectl -n prod apply -f - <<'EOF'
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: web-from-dev-api
|
|
spec:
|
|
podSelector:
|
|
matchLabels: {app: web}
|
|
policyTypes: [Ingress]
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels: {team: dev}
|
|
podSelector:
|
|
matchLabels: {app: api}
|
|
ports:
|
|
- {protocol: TCP, port: 80}
|
|
EOF
|
|
kubectl -n dev exec api -- wget -qO- -T3 web.prod
|
|
kubectl -n dev exec tester -- wget -qO- -T3 web.prod || echo blocked"""),
|
|
step("Egress, and DNS",
|
|
r"""Now limit what `tester` in `dev` may **send**: NetworkPolicy `tester-egress` in `dev` selects `app=tester` and allows egress only to `dev`'s `app=api` Pods on TCP 80 - and DNS, to `kube-dns` in `kube-system` on UDP and TCP 53.
|
|
|
|
`tester` reaches `api` by its name; `echo` is now out of its reach. Without the DNS rule, even `api` would fail - the name would not resolve.""",
|
|
check=NET + r"""
|
|
eq "$(j -n dev netpol tester-egress '{.spec.policyTypes[0]}')" Egress || exit 1
|
|
okr dev tester http://api && blocked dev tester http://echo""",
|
|
hint=r"""Two egress rules: `{to: [{podSelector: {matchLabels: {app: api}}}], ports: [{protocol: TCP, port: 80}]}` and `{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}, podSelector: {matchLabels: {k8s-app: kube-dns}}}], ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]}`. Every Namespace carries the label `kubernetes.io/metadata.name`.""",
|
|
show=r"""kubectl -n dev apply -f - <<'EOF'
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: tester-egress
|
|
spec:
|
|
podSelector:
|
|
matchLabels: {app: tester}
|
|
policyTypes: [Egress]
|
|
egress:
|
|
- to:
|
|
- podSelector:
|
|
matchLabels: {app: api}
|
|
ports:
|
|
- {protocol: TCP, port: 80}
|
|
- to:
|
|
- namespaceSelector:
|
|
matchLabels: {kubernetes.io/metadata.name: kube-system}
|
|
podSelector:
|
|
matchLabels: {k8s-app: kube-dns}
|
|
ports:
|
|
- {protocol: UDP, port: 53}
|
|
- {protocol: TCP, port: 53}
|
|
EOF
|
|
kubectl -n dev exec tester -- wget -qO- -T3 api
|
|
kubectl -n dev exec tester -- wget -qO- -T3 echo || echo blocked"""),
|
|
],
|
|
"outro": r"""You closed a Namespace, opened single paths into it, used the AND form of a rule, and limited a Pod's egress without breaking its DNS.
|
|
|
|
**For the exam**
|
|
|
|
- Test every policy from the Pods in question: `kubectl exec POD -- wget -qO- -T3 TARGET` (or `nc -zv -w3 HOST PORT`). A timeout means dropped.
|
|
- `policyTypes` decides which directions the policy restricts; a policy with only `Ingress` leaves egress open.
|
|
- Ports in a policy are the Pod's ports (`targetPort`), not the Service's.
|
|
- `ipBlock: {cidr: ..., except: [...]}` for addresses outside the cluster.
|
|
- `kubectl describe netpol` spells the rules out in words - good for checking the AND/OR.
|
|
|
|
Next, prove it against the clock: **drill 5.2**.""",
|
|
}
|
|
|
|
# ---- 5.3 Services and endpoints ---------------------------------------------------------------
|
|
L53 = {
|
|
"id": 9503, "objective": "5.3", "workers": 1,
|
|
"title": "Service types, headless Services, ExternalName, and endpoints by hand",
|
|
"intro": r"""A Service is a stable name and address in front of a changing set of Pods - its **endpoints**, kept in EndpointSlices by a controller that follows the selector:
|
|
|
|
```
|
|
ClusterIP a virtual IP inside the cluster the default
|
|
NodePort ClusterIP + a port (30000-32767) on every node
|
|
LoadBalancer NodePort + an external address from a cloud controller (none here: it stays <pending>)
|
|
headless clusterIP: None - DNS returns the Pod IPs themselves
|
|
ExternalName a DNS alias (CNAME) for a name outside the cluster; no proxying
|
|
no selector you write the EndpointSlice yourself - for something outside the cluster
|
|
```
|
|
|
|
Namespace `svc` runs Deployment `web` (nginx, 2 replicas), Pod `client`, and Pod `legacy-box`, which serves on port 80 but carries no label any Service selects.""",
|
|
"setup": PODS + r"""
|
|
mkns svc
|
|
mkdir -p "$WS/lesson-5.3"
|
|
$K -n svc create deploy web --image=nginx:1.27 --replicas=2 --port=80 >/dev/null
|
|
cli svc client
|
|
$K -n svc run legacy-box --image=busybox:1.36 --labels=role=legacy --command -- sh -c 'mkdir -p /www && echo legacy > /www/index.html && exec httpd -f -p 80 -h /www' >/dev/null
|
|
$K -n svc rollout status deploy web --timeout=180s >/dev/null && ready svc
|
|
""",
|
|
"steps": [
|
|
step("ClusterIP",
|
|
r"""Expose Deployment `web` as Service `web`, port 80. From `client`, `wget -qO- web` returns nginx's page.""",
|
|
check=NET + r"""eq "$(j -n svc svc web '{.spec.type} {.spec.ports[0].port}')" "ClusterIP 80" && okr svc client http://web""",
|
|
hint=r"""`kubectl expose deploy web --port=80` - it copies the Deployment's selector.""",
|
|
show=r"""kubectl -n svc expose deploy web --port=80
|
|
kubectl -n svc get svc,endpointslices
|
|
kubectl -n svc exec client -- wget -qO- -T3 web | head -4"""),
|
|
step("NodePort",
|
|
r"""Create Service `web-np` of type **NodePort** for the same Pods, with node port **30080**. Then reach it from the control-plane node: `curl http://{W1}-ADDRESS:30080` - every node answers on that port, whether or not it runs one of the Pods.""",
|
|
check=r"""
|
|
eq "$(j -n svc svc web-np '{.spec.type} {.spec.ports[0].nodePort}')" "NodePort 30080" || exit 1
|
|
ip=$(j node "$W1" '{.status.addresses[?(@.type=="InternalIP")].address}'); nx "$CP" "curl -s -m 3 http://$ip:30080 | grep -q nginx" </dev/null""",
|
|
hint=r"""`kubectl create service nodeport web-np --tcp=80:80 --node-port=30080` makes a selector `app=web-np` - change it to `app=web`; or write the YAML, or `kubectl expose deploy web --name=web-np --type=NodePort` and then set the `nodePort`.""",
|
|
show=r"""kubectl -n svc expose deploy web --name=web-np --type=NodePort --port=80 --dry-run=client -o yaml > {WS}/lesson-5.3/web-np.yaml
|
|
yq -i '.spec.ports[0].nodePort = 30080' {WS}/lesson-5.3/web-np.yaml
|
|
kubectl -n svc apply -f {WS}/lesson-5.3/web-np.yaml
|
|
ip=$(kubectl get node {W1} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
|
|
curl -s -m 3 http://$ip:30080 | head -4"""),
|
|
step("Headless",
|
|
r"""Create Service `web-headless` with `clusterIP: None` for the `web` Pods. Look it up from `client` with `nslookup web-headless`: you get the two Pod IPs, not a Service IP. Write them, one per line, to `{WS}/lesson-5.3/headless.txt`.""",
|
|
check=DNS + r"""
|
|
eq "$(j -n svc svc web-headless '{.spec.clusterIP}')" None || exit 1
|
|
want=$($K -n svc get pods -l app=web -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | sort)
|
|
eq "$(tr -d ' \r' < "$WS/lesson-5.3/headless.txt" 2>/dev/null | grep . | sort)" "$want" """,
|
|
hint=r"""`kubectl -n svc expose deploy web --name=web-headless --cluster-ip=None --port=80`. In busybox's `nslookup` output the addresses follow the `Name:` line.""",
|
|
show=r"""kubectl -n svc expose deploy web --name=web-headless --cluster-ip=None --port=80
|
|
sleep 5
|
|
kubectl -n svc exec client -- nslookup web-headless
|
|
kubectl -n svc exec client -- nslookup web-headless | awk '/^Name:/ {n=1} n && /^Address/ {print $NF}' > {WS}/lesson-5.3/headless.txt
|
|
cat {WS}/lesson-5.3/headless.txt"""),
|
|
step("ExternalName",
|
|
r"""Create Service `docs` of type **ExternalName** pointing at `kubernetes.io`. Inside the cluster, `docs.svc.svc.cluster.local` is now a CNAME for that name - DNS only: nothing is proxied, and the port does not matter.""",
|
|
check=r"""eq "$(j -n svc svc docs '{.spec.type} {.spec.externalName}')" "ExternalName kubernetes.io" """,
|
|
hint=r"""`kubectl -n svc create service externalname docs --external-name=kubernetes.io`.""",
|
|
show=r"""kubectl -n svc create service externalname docs --external-name=kubernetes.io
|
|
kubectl -n svc get svc docs
|
|
kubectl -n svc exec client -- nslookup docs | head -6"""),
|
|
step("A Service without a selector",
|
|
r"""Create Service `legacy` (port 80) with **no selector**, and an EndpointSlice for it by hand that holds `legacy-box`'s Pod IP on port 80. From `client`, `wget -qO- legacy` then answers `legacy`.
|
|
|
|
An EndpointSlice belongs to a Service by the label `kubernetes.io/service-name`; its port's name must match the Service port's name (here: none).""",
|
|
check=NET + r"""
|
|
[ -z "$(j -n svc svc legacy '{.spec.selector}')" ] || exit 1
|
|
for i in 1 2 3 4 5; do [ "$($K -n svc exec client -- wget -qO- -T3 legacy 2>/dev/null)" = legacy ] && exit 0; sleep 3; done; exit 1""",
|
|
hint=r"""Service: `spec.ports: [{port: 80, targetPort: 80}]` and no `selector`. EndpointSlice (`discovery.k8s.io/v1`): `addressType: IPv4`, `endpoints: [{addresses: [IP]}]`, `ports: [{port: 80, protocol: TCP}]`, and the label.""",
|
|
show=r"""ip=$(kubectl -n svc get pod legacy-box -o jsonpath='{.status.podIP}')
|
|
kubectl -n svc apply -f - <<EOF
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: legacy
|
|
spec:
|
|
ports:
|
|
- {port: 80, targetPort: 80, protocol: TCP}
|
|
---
|
|
apiVersion: discovery.k8s.io/v1
|
|
kind: EndpointSlice
|
|
metadata:
|
|
name: legacy-1
|
|
labels:
|
|
kubernetes.io/service-name: legacy
|
|
addressType: IPv4
|
|
endpoints:
|
|
- addresses: ["$ip"]
|
|
ports:
|
|
- {port: 80, protocol: TCP}
|
|
EOF
|
|
kubectl -n svc exec client -- wget -qO- -T3 legacy"""),
|
|
],
|
|
"outro": r"""You exposed the same Pods four ways, saw what headless DNS returns, aliased an outside name, and backed a Service with endpoints you wrote yourself.
|
|
|
|
**For the exam**
|
|
|
|
- `kubectl expose deploy|pod|svc NAME --port --target-port --type --name` copies the selector; `kubectl create service clusterip|nodeport|loadbalancer|externalname` does not know your Pods.
|
|
- `kubectl get endpointslices -l kubernetes.io/service-name=NAME` - empty means the selector matches no ready Pod.
|
|
- NodePorts are 30000-32767 by default and open on every node; `externalTrafficPolicy: Local` keeps traffic on nodes that run a Pod.
|
|
- A Service's port may be named; then `targetPort` can be the container port's **name**, and EndpointSlice ports must carry the same name.
|
|
|
|
Next, prove it against the clock: **drill 5.3**.""",
|
|
}
|
|
|
|
# ---- 5.4 Gateway API --------------------------------------------------------------------------
|
|
L54 = {
|
|
"id": 9504, "objective": "5.4", "workers": 1,
|
|
"title": "Gateway API: GatewayClass, Gateway, HTTPRoute - paths, weights, headers, TLS",
|
|
"intro": r"""The Gateway API splits what Ingress kept in one object across three roles:
|
|
|
|
```
|
|
GatewayClass which controller implements gateways (cluster-wide; the infrastructure provider)
|
|
Gateway an entry point: listeners - protocol, port, hostname, TLS - and which routes may attach
|
|
HTTPRoute routing rules: matches (path, header, method, query) -> backendRefs (Services, with weights)
|
|
```
|
|
|
|
A route attaches to a Gateway through `parentRefs`; a listener's `allowedRoutes` says from which Namespaces. Weights split traffic between backends - a canary without a second Deployment of tricks.
|
|
|
|
The Gateway API's CRDs are installed. No controller runs here, so nothing will actually serve traffic: you are graded, as in many exam tasks, on the objects. Namespace `gw` has Services `web` (port 80), `web-v2` (80) and `api` (8080).""",
|
|
"setup": NS + r"""
|
|
ensure_gateway_crds || { echo "could not install the Gateway API CRDs" >&2; exit 1; }
|
|
mkns gw
|
|
for s in web:80 web-v2:80 api:8080; do $K -n gw create service clusterip "${s%%:*}" --tcp="${s#*:}:80" >/dev/null; done
|
|
mkdir -p "$WS/lesson-5.4"
|
|
""",
|
|
"steps": [
|
|
step("A class and a Gateway",
|
|
r"""Create GatewayClass `example` (controller name `example.com/gateway-controller`) and, in `gw`, Gateway `shop` of that class with one listener: `http`, protocol HTTP, port 80, hostname `shop.example.com`, accepting routes from its own Namespace.""",
|
|
check=r"""
|
|
eq "$(j gatewayclass example '{.spec.controllerName}')" example.com/gateway-controller || exit 1
|
|
eq "$(j -n gw gateway shop '{.spec.gatewayClassName} {.spec.listeners[0].name} {.spec.listeners[0].protocol} {.spec.listeners[0].port} {.spec.listeners[0].hostname} {.spec.listeners[0].allowedRoutes.namespaces.from}')" "example http HTTP 80 shop.example.com Same" """,
|
|
hint=r"""Both are `gateway.networking.k8s.io/v1`. `kubectl explain gateway.spec.listeners` once the CRDs are in; the docs at gateway-api.sigs.k8s.io have every example.""",
|
|
show=r"""kubectl apply -f - <<'EOF'
|
|
apiVersion: gateway.networking.k8s.io/v1
|
|
kind: GatewayClass
|
|
metadata:
|
|
name: example
|
|
spec:
|
|
controllerName: example.com/gateway-controller
|
|
---
|
|
apiVersion: gateway.networking.k8s.io/v1
|
|
kind: Gateway
|
|
metadata:
|
|
name: shop
|
|
namespace: gw
|
|
spec:
|
|
gatewayClassName: example
|
|
listeners:
|
|
- name: http
|
|
protocol: HTTP
|
|
port: 80
|
|
hostname: shop.example.com
|
|
allowedRoutes:
|
|
namespaces:
|
|
from: Same
|
|
EOF
|
|
kubectl -n gw get gateway shop"""),
|
|
step("Route by path",
|
|
r"""Create HTTPRoute `shop` in `gw`, attached to Gateway `shop`: requests for paths under `/api` go to Service `api` port 8080, everything else to `web` port 80.""",
|
|
check=r"""
|
|
py() { $K -n gw get httproute shop -o json 2>/dev/null | python3 -c "import json,sys; r=json.load(sys.stdin)['spec']; $1"; }
|
|
py '
|
|
p = r["parentRefs"][0]["name"]; rules = r["rules"]
|
|
def to(path):
|
|
for x in rules:
|
|
for m in x.get("matches", []):
|
|
if m.get("path", {}).get("value") == path and m["path"].get("type", "PathPrefix") == "PathPrefix":
|
|
return [(b["name"], b.get("port")) for b in x["backendRefs"]]
|
|
sys.exit(0 if p == "shop" and to("/api") == [("api", 8080)] and to("/") == [("web", 80)] else 1)'""",
|
|
hint=r"""`parentRefs: [{name: shop}]`, then two `rules`, each with `matches: [{path: {type: PathPrefix, value: ...}}]` and `backendRefs: [{name: ..., port: ...}]`.""",
|
|
show=r"""kubectl -n gw apply -f - <<'EOF'
|
|
apiVersion: gateway.networking.k8s.io/v1
|
|
kind: HTTPRoute
|
|
metadata:
|
|
name: shop
|
|
spec:
|
|
parentRefs:
|
|
- name: shop
|
|
hostnames: [shop.example.com]
|
|
rules:
|
|
- matches:
|
|
- path: {type: PathPrefix, value: /api}
|
|
backendRefs:
|
|
- {name: api, port: 8080}
|
|
- matches:
|
|
- path: {type: PathPrefix, value: /}
|
|
backendRefs:
|
|
- {name: web, port: 80}
|
|
EOF
|
|
kubectl -n gw describe httproute shop | grep -A3 'Backend Refs' | head -8"""),
|
|
step("A canary by weight",
|
|
r"""Change the `/` rule of route `shop` so that **90%** of its requests go to `web` and **10%** to `web-v2` (both port 80). The `/api` rule stays as it is.""",
|
|
check=r"""
|
|
$K -n gw get httproute shop -o json 2>/dev/null | python3 -c '
|
|
import json, sys; rules = json.load(sys.stdin)["spec"]["rules"]
|
|
def by(path): return next((x for x in rules for m in x.get("matches", []) if m.get("path", {}).get("value") == path), None)
|
|
root, api = by("/"), by("/api")
|
|
w = sorted((b["name"], b.get("weight", 1)) for b in root["backendRefs"]) if root else []
|
|
sys.exit(0 if w == [("web", 90), ("web-v2", 10)] and api and [b["name"] for b in api["backendRefs"]] == ["api"] else 1)'""",
|
|
hint=r"""Two `backendRefs` in the same rule, each with a `weight`. The weights are relative: 9 and 1 would do the same.""",
|
|
show=r"""kubectl -n gw get httproute shop -o yaml > {WS}/lesson-5.4/shop.yaml
|
|
yq -i '(.spec.rules[] | select(.matches[0].path.value == "/")).backendRefs = [{"name": "web", "port": 80, "weight": 90}, {"name": "web-v2", "port": 80, "weight": 10}]' {WS}/lesson-5.4/shop.yaml
|
|
kubectl apply -f {WS}/lesson-5.4/shop.yaml
|
|
kubectl -n gw get httproute shop -o jsonpath='{.spec.rules[1].backendRefs}'; echo"""),
|
|
step("Beta testers by header",
|
|
r"""Add HTTPRoute `shop-beta` in `gw`, attached to `shop`: requests carrying the header `x-env: beta` go **all** to `web-v2`. A more specific match wins, so these requests no longer take part in the 90/10 split.""",
|
|
check=r"""
|
|
$K -n gw get httproute shop-beta -o json 2>/dev/null | python3 -c '
|
|
import json, sys; s = json.load(sys.stdin)["spec"]; r = s["rules"][0]
|
|
h = r["matches"][0].get("headers", [{}])[0]
|
|
sys.exit(0 if s["parentRefs"][0]["name"] == "shop" and h.get("name", "").lower() == "x-env" and h.get("value") == "beta" and [b["name"] for b in r["backendRefs"]] == ["web-v2"] else 1)'""",
|
|
hint=r"""`matches: [{headers: [{name: x-env, value: beta}]}]` (type `Exact` by default).""",
|
|
show=r"""kubectl -n gw apply -f - <<'EOF'
|
|
apiVersion: gateway.networking.k8s.io/v1
|
|
kind: HTTPRoute
|
|
metadata:
|
|
name: shop-beta
|
|
spec:
|
|
parentRefs:
|
|
- name: shop
|
|
hostnames: [shop.example.com]
|
|
rules:
|
|
- matches:
|
|
- headers:
|
|
- {name: x-env, value: beta}
|
|
backendRefs:
|
|
- {name: web-v2, port: 80}
|
|
EOF
|
|
kubectl -n gw get httproute"""),
|
|
step("HTTPS on the Gateway",
|
|
r"""Make a self-signed certificate for `shop.example.com`, store it as TLS Secret `shop-tls` in `gw`, and add a second listener to Gateway `shop`: `https`, protocol HTTPS, port 443, the same hostname, TLS mode `Terminate` with that Secret as its certificate.""",
|
|
check=r"""
|
|
eq "$(j -n gw secret shop-tls '{.type}')" kubernetes.io/tls || exit 1
|
|
eq "$(j -n gw gateway shop '{.spec.listeners[?(@.name=="https")].protocol} {.spec.listeners[?(@.name=="https")].port} {.spec.listeners[?(@.name=="https")].tls.mode} {.spec.listeners[?(@.name=="https")].tls.certificateRefs[0].name}')" "HTTPS 443 Terminate shop-tls" \
|
|
&& eq "$(j -n gw gateway shop '{.spec.listeners[?(@.name=="http")].port}')" 80""",
|
|
hint=r"""`openssl req -x509 ...`, `kubectl create secret tls`, then a listener with `tls: {mode: Terminate, certificateRefs: [{name: shop-tls}]}`. Keep the `http` listener.""",
|
|
show=r"""cd {WS}/lesson-5.4
|
|
openssl req -x509 -newkey rsa:2048 -nodes -keyout tls.key -out tls.crt -days 30 -subj /CN=shop.example.com 2>/dev/null
|
|
kubectl -n gw create secret tls shop-tls --cert=tls.crt --key=tls.key
|
|
kubectl -n gw patch gateway shop --type json -p '[{"op":"add","path":"/spec/listeners/-","value":{"name":"https","protocol":"HTTPS","port":443,"hostname":"shop.example.com","tls":{"mode":"Terminate","certificateRefs":[{"name":"shop-tls"}]},"allowedRoutes":{"namespaces":{"from":"Same"}}}}]'
|
|
kubectl -n gw get gateway shop -o jsonpath='{range .spec.listeners[*]}{.name} {.port}{"\n"}{end}'
|
|
cd"""),
|
|
],
|
|
"outro": r"""You built a Gateway with HTTP and HTTPS listeners, routed by path, split traffic by weight, and sent one header's requests elsewhere.
|
|
|
|
**For the exam**
|
|
|
|
- The CRDs come from the Gateway API release (`standard-install.yaml`); the controller is separate (Envoy Gateway, NGINX Gateway Fabric, Istio, Cilium, ...). A Gateway's `status` stays empty without one.
|
|
- `parentRefs` names the Gateway (and `sectionName` a listener); `allowedRoutes.namespaces.from` is `Same`, `All` or `Selector`.
|
|
- Matches: `path` (`PathPrefix`, `Exact`, `RegularExpression`), `headers`, `queryParams`, `method`. Filters: `RequestRedirect` (http → https), `URLRewrite`, `RequestHeaderModifier`.
|
|
- Moving from Ingress: one Ingress rule ≈ one HTTPRoute rule; TLS moves from the Ingress to the Gateway's listener.
|
|
|
|
Next, prove it against the clock: **drill 5.4**.""",
|
|
}
|
|
|
|
# ---- 5.5 Ingress ------------------------------------------------------------------------------
|
|
L55 = {
|
|
"id": 9505, "objective": "5.5", "workers": 1,
|
|
"title": "Ingress: classes, host and path rules, TLS, and the default backend",
|
|
"intro": r"""An **Ingress** routes HTTP(S) by host and path to Services. It does nothing on its own: an **Ingress controller** (ingress-nginx, Traefik, HAProxy, ...) watches Ingresses of its **IngressClass** and configures a proxy from them.
|
|
|
|
```
|
|
ingressClassName which controller (or the IngressClass marked default)
|
|
rules[].host shop.example.com (none = every host)
|
|
paths[].pathType Prefix | Exact | ImplementationSpecific
|
|
paths[].backend service: {name, port: {number | name}}
|
|
tls[] hosts + a kubernetes.io/tls Secret
|
|
defaultBackend what matches no rule
|
|
```
|
|
|
|
No controller runs here, so you are graded on the objects. Namespace `ing` has Services `web` (port 80), `api` (8080) and `health` (8081).""",
|
|
"setup": NS + r"""
|
|
mkns ing
|
|
for s in web:80 api:8080 health:8081; do $K -n ing create service clusterip "${s%%:*}" --tcp="${s#*:}:80" >/dev/null; done
|
|
$K delete ingressclass nginx --ignore-not-found >/dev/null 2>&1
|
|
mkdir -p "$WS/lesson-5.5"
|
|
""",
|
|
"steps": [
|
|
step("An IngressClass, the default",
|
|
r"""Create IngressClass `nginx` for controller `k8s.io/ingress-nginx`, and make it the cluster's **default** class: an Ingress without `ingressClassName` then belongs to it.""",
|
|
check=r"""eq "$(j ingressclass nginx '{.spec.controller} {.metadata.annotations.ingressclass\.kubernetes\.io/is-default-class}')" "k8s.io/ingress-nginx true" """,
|
|
hint=r"""`networking.k8s.io/v1`, kind `IngressClass`, `spec.controller`, and the annotation `ingressclass.kubernetes.io/is-default-class: "true"`. Installing ingress-nginx makes this object for you.""",
|
|
show=r"""kubectl apply -f - <<'EOF'
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: IngressClass
|
|
metadata:
|
|
name: nginx
|
|
annotations:
|
|
ingressclass.kubernetes.io/is-default-class: "true"
|
|
spec:
|
|
controller: k8s.io/ingress-nginx
|
|
EOF
|
|
kubectl get ingressclass"""),
|
|
step("Host and paths, from the command line",
|
|
r"""Create Ingress `shop` in `ing` with class `nginx` and host `shop.example.com`: paths under `/api` go to `api:8080`, everything else to `web:80`. `kubectl create ingress` can do it in one line.""",
|
|
check=r"""
|
|
$K -n ing get ingress shop -o json 2>/dev/null | python3 -c '
|
|
import json, sys; s = json.load(sys.stdin)["spec"]; r = s["rules"][0]
|
|
p = {x["path"]: (x["pathType"], x["backend"]["service"]["name"], x["backend"]["service"]["port"]["number"]) for x in r["http"]["paths"]}
|
|
sys.exit(0 if s.get("ingressClassName") == "nginx" and r["host"] == "shop.example.com" and p.get("/api") == ("Prefix", "api", 8080) and p.get("/") == ("Prefix", "web", 80) else 1)'""",
|
|
hint=r"""`kubectl create ingress shop --class=nginx --rule='shop.example.com/api*=api:8080' --rule='shop.example.com/*=web:80'` - a trailing `*` means `Prefix`, without it `Exact`.""",
|
|
show=r"""kubectl -n ing create ingress shop --class=nginx --rule='shop.example.com/api*=api:8080' --rule='shop.example.com/*=web:80'
|
|
kubectl -n ing describe ingress shop | sed -n '/Rules/,/Annotations/p'"""),
|
|
step("An exact path",
|
|
r"""Add a rule to Ingress `shop`: exactly `/healthz` (pathType `Exact`) goes to `health:8081`. `/healthz/deep` would not match it - it would fall through to the `/` prefix.""",
|
|
check=r"""
|
|
$K -n ing get ingress shop -o json 2>/dev/null | python3 -c '
|
|
import json, sys; ps = json.load(sys.stdin)["spec"]["rules"][0]["http"]["paths"]
|
|
h = [x for x in ps if x["path"] == "/healthz"]
|
|
sys.exit(0 if h and h[0]["pathType"] == "Exact" and h[0]["backend"]["service"]["name"] == "health" and h[0]["backend"]["service"]["port"]["number"] == 8081 and len(ps) == 3 else 1)'""",
|
|
hint=r"""`kubectl edit ingress shop` and add a third entry under `paths:`, or a JSON patch that appends to `/spec/rules/0/http/paths`.""",
|
|
show=r"""kubectl -n ing patch ingress shop --type json -p '[{"op":"add","path":"/spec/rules/0/http/paths/-","value":{"path":"/healthz","pathType":"Exact","backend":{"service":{"name":"health","port":{"number":8081}}}}}]'
|
|
kubectl -n ing get ingress shop -o jsonpath='{range .spec.rules[0].http.paths[*]}{.pathType} {.path} -> {.backend.service.name}{"\n"}{end}'"""),
|
|
step("TLS",
|
|
r"""Make a self-signed certificate for `shop.example.com`, store it as TLS Secret `shop-tls` in `ing`, and add a `tls` section to Ingress `shop` for that host with that Secret. The controller would then serve HTTPS for it and, by default, redirect HTTP there.""",
|
|
check=r"""
|
|
eq "$(j -n ing secret shop-tls '{.type}')" kubernetes.io/tls && eq "$(j -n ing ingress shop '{.spec.tls[0].hosts[0]} {.spec.tls[0].secretName}')" "shop.example.com shop-tls" \
|
|
&& j -n ing secret shop-tls '{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject 2>/dev/null | grep -q shop.example.com""",
|
|
hint=r"""`spec.tls: [{hosts: [shop.example.com], secretName: shop-tls}]`. The Secret must be in the Ingress's Namespace.""",
|
|
show=r"""cd {WS}/lesson-5.5
|
|
openssl req -x509 -newkey rsa:2048 -nodes -keyout tls.key -out tls.crt -days 30 -subj /CN=shop.example.com 2>/dev/null
|
|
kubectl -n ing create secret tls shop-tls --cert=tls.crt --key=tls.key
|
|
kubectl -n ing patch ingress shop --type merge -p '{"spec":{"tls":[{"hosts":["shop.example.com"],"secretName":"shop-tls"}]}}'
|
|
kubectl -n ing get ingress shop
|
|
cd"""),
|
|
step("What matches nothing",
|
|
r"""Give Ingress `shop` a **default backend**: `web:80`, for requests whose host or path no rule matches.""",
|
|
check=r"""eq "$(j -n ing ingress shop '{.spec.defaultBackend.service.name} {.spec.defaultBackend.service.port.number}')" "web 80" """,
|
|
hint=r"""`spec.defaultBackend: {service: {name: web, port: {number: 80}}}`; `kubectl create ingress --default-backend=web:80` does it at creation.""",
|
|
show=r"""kubectl -n ing patch ingress shop --type merge -p '{"spec":{"defaultBackend":{"service":{"name":"web","port":{"number":80}}}}}'
|
|
kubectl -n ing describe ingress shop | head -12"""),
|
|
],
|
|
"outro": r"""You made a default IngressClass and an Ingress with host, prefix and exact paths, TLS and a default backend.
|
|
|
|
**For the exam**
|
|
|
|
- `kubectl create ingress NAME --class=C --rule='HOST/PATH*=SVC:PORT[,tls=SECRET]' [--default-backend=SVC:PORT]`.
|
|
- `pathType` is required; `Prefix` matches by path segment (`/api` matches `/api/v1`, not `/apix`).
|
|
- A backend port is the **Service's** port, by number or name.
|
|
- No address in `kubectl get ingress`? The controller is not running, or the class names another controller. Controller-specific behaviour lives in annotations (`nginx.ingress.kubernetes.io/rewrite-target`, `ssl-redirect`).
|
|
|
|
Next, prove it against the clock: **drill 5.5**.""",
|
|
}
|
|
|
|
# ---- 5.6 CoreDNS ------------------------------------------------------------------------------
|
|
L56 = {
|
|
"id": 9506, "objective": "5.6", "workers": 1,
|
|
"title": "Cluster DNS: names of Services and Pods, the Corefile, and a Pod's resolver",
|
|
"intro": r"""CoreDNS (Deployment `coredns`, Service `kube-dns` in `kube-system`) answers every Pod's DNS queries. The names it knows:
|
|
|
|
```
|
|
SERVICE.NAMESPACE.svc.cluster.local the ClusterIP (or, headless, the Pod IPs)
|
|
POD.SERVICE.NAMESPACE.svc.cluster.local one Pod of a headless Service - a StatefulSet's db-0.db...
|
|
A-B-C-D.NAMESPACE.pod.cluster.local a Pod by its IP, dots as dashes
|
|
```
|
|
|
|
Each Pod's `/etc/resolv.conf` points at `kube-dns` and searches its own Namespace first, so a bare `web` works inside the Namespace and `web.other` across. CoreDNS itself is configured by the **Corefile** in ConfigMap `coredns`, and reloads it on its own when it changes.
|
|
|
|
Namespace `dns-lab` has Service `web`, a StatefulSet `db` (2 replicas) behind headless Service `db`, and Pod `client`.""",
|
|
"setup": PODS + r"""
|
|
mkns dns-lab
|
|
mkdir -p "$WS/lesson-5.6"
|
|
srv dns-lab web; cli dns-lab client
|
|
$K -n dns-lab apply -f - >/dev/null <<'EOF'
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata: {name: db}
|
|
spec:
|
|
clusterIP: None
|
|
selector: {app: db}
|
|
ports: [{port: 5432}]
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata: {name: db}
|
|
spec:
|
|
serviceName: db
|
|
replicas: 2
|
|
selector: {matchLabels: {app: db}}
|
|
template:
|
|
metadata: {labels: {app: db}}
|
|
spec:
|
|
containers: [{name: db, image: busybox:1.36, command: [sleep, '86400']}]
|
|
EOF
|
|
$K -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' > "$PRIV/lesson-5.6.corefile"
|
|
ready dns-lab && $K -n dns-lab rollout status sts db --timeout=180s >/dev/null
|
|
""",
|
|
"steps": [
|
|
step("A Service by its full name",
|
|
r"""From `client`, look up `web` by its **fully qualified** name, and write the address you get to `{WS}/lesson-5.6/web.txt`. It is the Service's ClusterIP.""",
|
|
check=r"""eq "$(tr -d ' \r\n' < "$WS/lesson-5.6/web.txt" 2>/dev/null)" "$(j -n dns-lab svc web '{.spec.clusterIP}')" """,
|
|
hint=r"""`kubectl -n dns-lab exec client -- nslookup web.dns-lab.svc.cluster.local`.""",
|
|
show=r"""kubectl -n dns-lab exec client -- nslookup web.dns-lab.svc.cluster.local
|
|
kubectl -n dns-lab exec client -- nslookup web.dns-lab.svc.cluster.local | awk '/^Name:/ {n=1} n && /^Address/ {print $NF; exit}' > {WS}/lesson-5.6/web.txt"""),
|
|
step("One Pod of a StatefulSet",
|
|
r"""A StatefulSet's Pods get stable names through their headless Service. Write the fully qualified DNS name of Pod `db-0` to `{WS}/lesson-5.6/db0.txt`, and check from `client` that it resolves to `db-0`'s IP.""",
|
|
check=DNS + r"""
|
|
eq "$(tr -d ' \r\n' < "$WS/lesson-5.6/db0.txt" 2>/dev/null | sed 's/\.$//')" db-0.db.dns-lab.svc.cluster.local && resolves dns-lab client db-0.db.dns-lab.svc.cluster.local "$(j -n dns-lab pod db-0 '{.status.podIP}')" """,
|
|
hint=r"""`POD.SERVICE.NAMESPACE.svc.cluster.local` - the Service is the StatefulSet's `serviceName`.""",
|
|
show=r"""echo db-0.db.dns-lab.svc.cluster.local > {WS}/lesson-5.6/db0.txt
|
|
kubectl -n dns-lab exec client -- nslookup db-0.db.dns-lab.svc.cluster.local
|
|
kubectl -n dns-lab get pod db-0 -o wide"""),
|
|
step("Names of your own",
|
|
r"""Add a `hosts` block to CoreDNS's Corefile so that every Pod resolves **`legacy.internal`** to `10.96.0.250`, and every other name still resolves as before (`fallthrough`). Put it inside the `.:53 { ... }` server block, before `forward`.
|
|
|
|
CoreDNS reloads the changed ConfigMap by itself within a minute; `client` then resolves the new name.""",
|
|
check=DNS + r"""
|
|
$K -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' | grep -q 'kubernetes cluster.local' || exit 1
|
|
resolves dns-lab client legacy.internal 10.96.0.250 && resolves dns-lab client web.dns-lab.svc.cluster.local "$(j -n dns-lab svc web '{.spec.clusterIP}')" """,
|
|
hint=r"""`kubectl -n kube-system edit cm coredns`. The block: `hosts {` / `10.96.0.250 legacy.internal` / `fallthrough` / `}`. A syntax error leaves CoreDNS on its old configuration: `kubectl -n kube-system logs -l k8s-app=kube-dns` says so.""",
|
|
show=r"""kubectl -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' > {WS}/lesson-5.6/Corefile
|
|
sed -i '0,/^ forward/s// hosts {\n 10.96.0.250 legacy.internal\n fallthrough\n }\n forward/' {WS}/lesson-5.6/Corefile
|
|
cat {WS}/lesson-5.6/Corefile
|
|
kubectl -n kube-system create configmap coredns --from-file=Corefile={WS}/lesson-5.6/Corefile --dry-run=client -o yaml | kubectl apply -f -
|
|
sleep 40
|
|
kubectl -n dns-lab exec client -- nslookup legacy.internal"""),
|
|
step("A Pod's own resolver settings",
|
|
r"""Run Pod `tuned` in `dns-lab` (`busybox:1.36`, `sleep 86400`) whose resolver you set yourself: `dnsPolicy: None`, with the `kube-dns` Service's IP as its only nameserver, the search domains `dns-lab.svc.cluster.local` and `svc.cluster.local`, and the option `ndots: 2`. Its `/etc/resolv.conf` shows exactly that.""",
|
|
check=r"""
|
|
ip=$(j -n kube-system svc kube-dns '{.spec.clusterIP}')
|
|
r=$($K -n dns-lab exec tuned -- cat /etc/resolv.conf 2>/dev/null)
|
|
echo "$r" | grep -qx "nameserver $ip" && echo "$r" | grep -qx 'search dns-lab.svc.cluster.local svc.cluster.local' && echo "$r" | grep -q 'ndots:2' \
|
|
&& eq "$(j -n dns-lab pod tuned '{.spec.dnsPolicy}')" None""",
|
|
hint=r"""`spec.dnsPolicy: None` and `spec.dnsConfig: {nameservers: [IP], searches: [...], options: [{name: ndots, value: "2"}]}`. The other policies: `ClusterFirst` (the default), `Default` (the node's resolver), `ClusterFirstWithHostNet`.""",
|
|
show=r"""ip=$(kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}')
|
|
kubectl -n dns-lab apply -f - <<EOF
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: tuned
|
|
spec:
|
|
dnsPolicy: None
|
|
dnsConfig:
|
|
nameservers: [$ip]
|
|
searches: [dns-lab.svc.cluster.local, svc.cluster.local]
|
|
options:
|
|
- {name: ndots, value: "2"}
|
|
containers:
|
|
- name: tuned
|
|
image: busybox:1.36
|
|
command: [sleep, '86400']
|
|
EOF
|
|
kubectl -n dns-lab wait --for=condition=Ready pod/tuned --timeout=120s
|
|
kubectl -n dns-lab exec tuned -- cat /etc/resolv.conf"""),
|
|
],
|
|
"outro": r"""You resolved a Service and a StatefulSet Pod by their full names, taught CoreDNS a name of your own, and set a Pod's resolver by hand.
|
|
|
|
**For the exam**
|
|
|
|
- Test DNS from a Pod: `kubectl run tmp --rm -it --restart=Never --image=busybox:1.36 -- nslookup NAME`.
|
|
- Nothing resolves: are the `coredns` Pods running and ready, does Service `kube-dns` have endpoints, does a NetworkPolicy block UDP/TCP 53, is the Corefile valid (`kubectl -n kube-system logs -l k8s-app=kube-dns`)?
|
|
- Cluster names resolve but outside names do not: the `forward` line of the Corefile, or the node's `/etc/resolv.conf`.
|
|
- The kubelet's `clusterDNS` and `clusterDomain` decide what goes into every Pod's resolv.conf.
|
|
|
|
Next, prove it against the clock: **drill 5.6**.""",
|
|
}
|
|
|
|
LESSONS = [L51, L52, L53, L54, L55, L56]
|