"""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, cmd, hand # ---- 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=[cmd(r"""k get no -o custom-columns=NODE:.metadata.name,CIDR:.spec.podCIDR --no-headers > {WS}/lesson-5.1/cidrs.txt cat {WS}/lesson-5.1/cidrs.txt k -n net get po -o wide""", r"""Every Pod's IP is from the range of the node it runs on.""")]), 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=[cmd(r"""k -n net get po -o wide"""), hand(r"""k -n net exec a -- wget -qO- -T3 IP > {WS}/lesson-5.1/answer.txt""", r"""`IP`: Pod `b`'s address, from the list.""", run=r"""ip=$(kubectl -n net get pod b -o jsonpath='{.status.podIP}') kubectl -n net exec a -- wget -qO- -T3 http://$ip > {WS}/lesson-5.1/answer.txt"""), cmd(r"""cat {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 | 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=[cmd(r"""k get no -o wide k get no -o custom-columns=NODE:.metadata.name,CIDR:.spec.podCIDR""", r"""`{W2}`'s address (`INTERNAL-IP`) and its Pod CIDR."""), hand(r"""ssh {W1} ip route""", r"""The line for `{W2}`'s Pod CIDR reads `CIDR via ADDRESS dev ...` - the route to the other node's Pods.""", run=r"""ssh -n {W1} ip route"""), hand(r"""echo 'CIDR via ADDRESS' > {WS}/lesson-5.1/route.txt""", r"""The first three words of that line.""", run=r"""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""")]), 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=[cmd(r"""k -n net run hostnet --image=busybox:1.36 --dry-run=client -o yaml -- sleep 86400 > hostnet.yaml"""), hand(r"""vim hostnet.yaml""", r"""Add to the Pod's `spec:`, above `containers:`: ``` hostNetwork: true nodeName: {W1} ```""", run=r"""yq -i '.spec.hostNetwork = true | .spec.nodeName = "{W1}"' hostnet.yaml"""), cmd(r"""k -n net apply -f hostnet.yaml k -n net get po hostnet -o wide""", r"""Its IP is `{W1}`'s own address, not one from the Pod CIDR."""), hand(r"""echo IP > {WS}/lesson-5.1/hostnet.txt""", r"""`IP`: that address.""", run=r"""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"""), cmd(r"""k -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=[hand(r"""vim deny.yaml""", r"""No `kubectl create` for a NetworkPolicy: paste *Default deny all ingress traffic* from the docs page *Network Policies*, and name it: ``` apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny spec: podSelector: {} policyTypes: [Ingress] ```""", run=r"""cat > deny.yaml <<'EOF' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny spec: podSelector: {} policyTypes: [Ingress] EOF"""), cmd(r"""k -n prod apply -f deny.yaml k -n prod exec api -- wget -qO- -T3 db""", r"""It times out: 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=[hand(r"""vim db-from-api.yaml""", r"""Paste the example from the docs page *Network Policies* and cut it down to: ``` 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} ```""", run=r"""cat > db-from-api.yaml <<'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"""), cmd(r"""k -n prod apply -f db-from-api.yaml k -n prod exec api -- wget -qO- -T3 db k -n prod exec web -- wget -qO- -T3 db""", r"""`api` gets the page; `web` times out.""")]), 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=[hand(r"""cp db-from-api.yaml web-from-dev-api.yaml vim web-from-dev-api.yaml""", r"""Make it - `namespaceSelector` and `podSelector` in **one** element of `from` (no `-` before `podSelector`): ``` 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} ```""", run=r"""cat > web-from-dev-api.yaml <<'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"""), cmd(r"""k -n prod apply -f web-from-dev-api.yaml k -n dev exec api -- wget -qO- -T3 web.prod k -n dev exec tester -- wget -qO- -T3 web.prod""", r"""`dev`'s `api` gets in; `tester` times out.""")]), 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=[hand(r"""vim tester-egress.yaml""", r"""An egress policy - two elements of `egress`, one for `api`, one for DNS: ``` 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} ```""", run=r"""cat > tester-egress.yaml <<'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"""), cmd(r"""k -n dev apply -f tester-egress.yaml k -n dev exec tester -- wget -qO- -T3 api k -n dev exec tester -- wget -qO- -T3 echo""", r"""`api` answers; `echo` times out.""")]), ], "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 ) 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=[cmd(r"""k -n svc expose deploy web --port=80 k -n svc get svc,endpointslices k -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" web-np.yaml"""), hand(r"""vim web-np.yaml""", r"""Add to the port: ``` nodePort: 30080 ```""", run=r"""yq -i '.spec.ports[0].nodePort = 30080' web-np.yaml"""), cmd(r"""k -n svc apply -f web-np.yaml k get no -o wide""", r"""`{W1}`'s `INTERNAL-IP`:"""), hand(r"""curl IP:30080""", r"""nginx's page, from the node port.""", run=r"""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=[cmd(r"""k -n svc expose deploy web --name=web-headless --cluster-ip=None --port=80 k -n svc exec client -- nslookup web-headless""", r"""Two addresses: the Pods' own. Again in a few seconds if it does not resolve yet."""), hand(r"""vim {WS}/lesson-5.3/headless.txt""", r"""The two addresses, one per line.""", run=r"""sleep 5 kubectl -n svc exec client -- nslookup web-headless | awk '/^Name:/ {n=1} n && /^Address/ {print $NF}' > {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=[cmd(r"""k -n svc create svc externalname docs --external-name=kubernetes.io k -n svc get svc docs k -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=[cmd(r"""k -n svc get po legacy-box -o wide""", r"""Its IP goes into the EndpointSlice."""), hand(r"""vim legacy.yaml""", r"""No `kubectl create` makes a Service without a selector, nor an EndpointSlice: paste them from the docs page *Service* (*Services without selectors*), and make them, with that IP: ``` 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} ```""", run=r"""ip=$(kubectl -n svc get pod legacy-box -o jsonpath='{.status.podIP}') cat > legacy.yaml < 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=[hand(r"""vim gw.yaml""", r"""No `kubectl create` for these: paste a GatewayClass and a Gateway from gateway-api.sigs.k8s.io (*API overview*), and make them: ``` 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 ```""", run=r"""cat > gw.yaml <<'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"""), cmd(r"""k apply -f gw.yaml k -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=[hand(r"""vim route.yaml""", r"""Paste an HTTPRoute from gateway-api.sigs.k8s.io (*HTTP routing*), and make it: ``` 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} ```""", run=r"""cat > route.yaml <<'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"""), cmd(r"""k -n gw apply -f route.yaml k -n gw describe httproute shop""")]), 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=[hand(r"""k -n gw edit httproute shop""", r"""In the rule whose path is `/`, give `web` a `weight: 90` and add `web-v2` beside it: ``` - name: web port: 80 weight: 90 - name: web-v2 port: 80 weight: 10 ```""", run=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"""), cmd(r"""k -n gw describe httproute shop""")]), 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=[hand(r"""vim beta.yaml""", r"""Paste an HTTPRoute with a header match from gateway-api.sigs.k8s.io (*HTTP header matching*), and make it: ``` 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} ```""", run=r"""cat > beta.yaml <<'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"""), cmd(r"""k -n gw apply -f beta.yaml k -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=[cmd(r"""cd {WS}/lesson-5.4 openssl req -x509 -newkey rsa:2048 -nodes -keyout tls.key -out tls.crt -subj /CN=shop.example.com k -n gw create secret tls shop-tls --cert=tls.crt --key=tls.key cd"""), hand(r"""k -n gw edit gateway shop""", r"""Add a second listener below `http`, in `listeners:`: ``` - name: https protocol: HTTPS port: 443 hostname: shop.example.com tls: mode: Terminate certificateRefs: - name: shop-tls allowedRoutes: namespaces: from: Same ```""", run=r"""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"}}}}]' """), cmd(r"""k -n gw describe gateway shop | grep -A3 -i listener""")]), ], "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=[hand(r"""vim ic.yaml""", r"""No `kubectl create` for an IngressClass: paste the example from the docs page *Ingress* (*Default IngressClass*), and make it: ``` apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: nginx annotations: ingressclass.kubernetes.io/is-default-class: "true" spec: controller: k8s.io/ingress-nginx ```""", run=r"""cat > ic.yaml <<'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"""), cmd(r"""k apply -f ic.yaml k 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=[cmd(r"""k -n ing create ingress shop --class=nginx --rule='shop.example.com/api*=api:8080' --rule='shop.example.com/*=web:80' k -n ing describe ingress shop""")]), 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=[hand(r"""k -n ing edit ingress shop""", r"""Add a third path to `paths:`, indented like the other two: ``` - path: /healthz pathType: Exact backend: service: name: health port: number: 8081 ```""", run=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}}}}}]' """), cmd(r"""k -n ing describe ingress shop""")]), 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=[cmd(r"""cd {WS}/lesson-5.5 openssl req -x509 -newkey rsa:2048 -nodes -keyout tls.key -out tls.crt -subj /CN=shop.example.com k -n ing create secret tls shop-tls --cert=tls.crt --key=tls.key cd"""), hand(r"""k -n ing edit ingress shop""", r"""Add to `spec:`, beside `rules:`: ``` tls: - hosts: [shop.example.com] secretName: shop-tls ```""", run=r"""kubectl -n ing patch ingress shop --type merge -p '{"spec":{"tls":[{"hosts":["shop.example.com"],"secretName":"shop-tls"}]}}' """), cmd(r"""k -n ing get ingress shop""")]), 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=[hand(r"""k -n ing edit ingress shop""", r"""Add to `spec:`, beside `rules:`: ``` defaultBackend: service: name: web port: number: 80 ```""", run=r"""kubectl -n ing patch ingress shop --type merge -p '{"spec":{"defaultBackend":{"service":{"name":"web","port":{"number":80}}}}}' """), cmd(r"""k -n ing describe ingress shop""")]), ], "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=[cmd(r"""k -n dns-lab exec client -- nslookup web.dns-lab.svc.cluster.local""", r"""The `Address` below `Name:` is the Service's ClusterIP."""), hand(r"""echo IP > {WS}/lesson-5.6/web.txt""", r"""`IP`: that address.""", run=r"""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=[cmd(r"""echo db-0.db.dns-lab.svc.cluster.local > {WS}/lesson-5.6/db0.txt k -n dns-lab exec client -- nslookup db-0.db.dns-lab.svc.cluster.local k -n dns-lab get po 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=[hand(r"""k -n kube-system edit cm coredns""", r"""In `Corefile`, add above the `forward` line, indented like it: ``` hosts { 10.96.0.250 legacy.internal fallthrough } ```""", run=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 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"""), cmd(r"""k -n dns-lab exec client -- nslookup legacy.internal""", r"""Again until it answers `10.96.0.250`: CoreDNS reloads the ConfigMap by itself within a minute or two.""")]), 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=[cmd(r"""k -n kube-system get svc kube-dns""", r"""Its `CLUSTER-IP` is the nameserver."""), cmd(r"""k -n dns-lab run tuned --image=busybox:1.36 --dry-run=client -o yaml -- sleep 86400 > tuned.yaml"""), hand(r"""vim tuned.yaml""", r"""Change `dnsPolicy: ClusterFirst` to `dnsPolicy: None`, and add beside it (the docs page *DNS for Services and Pods* has it, under *Pod's DNS Config*): ``` dnsConfig: nameservers: [IP] searches: [dns-lab.svc.cluster.local, svc.cluster.local] options: - {name: ndots, value: "2"} ```""", run=r"""export IP=$(kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}') yq -i '.spec.dnsPolicy = "None" | .spec.dnsConfig = {"nameservers": [strenv(IP)], "searches": ["dns-lab.svc.cluster.local", "svc.cluster.local"], "options": [{"name": "ndots", "value": "2"}]}' tuned.yaml"""), cmd(r"""k -n dns-lab apply -f tuned.yaml k -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]