PlatformEngr® Kubernetes course

Module 00 · Orientation · 30 min · You are: Platform and product

Orientation: what a platform team owns in Kubernetes

In this module

  • You learn: What Kubernetes does (you declare what should run, controllers make it so), which parts of a shared cluster the platform team owns and which a product team owns, and what a namespace is.
  • You do: Tell the course your GitHub name, check your tools, create your repo k8s-platform, create a three-node cluster on your laptop, and declare the first namespace from a file.
  • You end with: A cluster kind-k8s with three Ready nodes. Your repo on GitHub holds cluster/kind.yaml and tenants/checkout/namespace.yaml. The namespace checkout-dev exists and is empty.

Most Kubernetes courses teach the objects one by one, as if you were alone on the cluster. A platform team is never alone: several teams share the cluster, and the work is deciding what each team may do, how much it may use, and what happens to its pods when a node goes away. You build that from the first module: a repo that says what should exist, a cluster that you only change from that repo, and one folder per team. You play the platform team and team-checkout, a product team.

Open this module in the interactive course to track progress.

Concepts

Declare, then reconcile
You send the API server an object that says what you want. A controller compares it with what exists and closes the gap, again and again. Nothing is "run"; things are made true.
Control plane and nodes
The control plane (API server, etcd, scheduler, controllers) decides. Nodes run a kubelet that starts containers. On kind every node is a Docker container on your laptop.
Namespace
A named slice of the cluster. Names, quotas, permissions and network rules attach to it. It is the unit a platform team hands to a product team.
Two owners
tenants/<team>/ is the platform team's: the namespace and the rules around it. apps/<team>/ is the product team's: what runs inside.
You are the reconciler
In this course a change is a commit followed by kubectl apply, which you run. Argo CD later does that step for you.

Who owns what in a shared cluster

ObjectFolderOwnerModule
Namespace, Pod Security labelstenants/<team>/Platform team00, 06
ResourceQuota, LimitRangetenants/<team>/Platform team03
Role, RoleBinding, ServiceAccounttenants/<team>/Platform team05
NetworkPolicytenants/<team>/Platform team06
Deployment, Service, Ingress, ConfigMap, PodDisruptionBudgetapps/<team>/Product team01 to 04
A Secret's valueNeither folderProduct team02
Nodes, ingress controller, network plugincluster/Platform team00, 01

Kubernetes does not care about folders. The folder is the ownership line people agree on.

Files to know

Where you are heading, in two minutes. These files from the reference repo describe the finished setup. Click Quick look on each: it opens the real file right here, with numbered circles on the lines that matter.

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Tell the course who you are

Your GitHub user (or organisation) and a folder for your repo and the reference clone. Every command and link uses them. They stay in your browser.

Set your GitHub user and workspace folder in the interactive course or the form at the top of the notes index; these notes then show your values.

2. Check your tools

Paste this into a terminal. Each tool reports ok (with its version), MISSING (with where to get it) or PROBLEM (installed but not usable yet, like Docker not running or gh not logged in). Install what is missing your usual way, then run it again until it says All set. Versions: kind 0.33 or newer (it runs the Kubernetes 1.37 node image, and its built-in network plugin enforces the network rules of module 06), kubectl 1.36 to 1.38, Docker with 4 GB of memory.

check() {  # tool, works-check, version, problem, where-to-get-it, optional
  if ! command -v "$1" >/dev/null 2>&1; then
    if [ -n "$6" ]; then printf '  optional  %-22s not installed (%s)\n' "$1" "$5"
    else printf '  MISSING   %-22s install it: %s\n' "$1" "$5"; bad=$((bad+1)); fi
  elif ! eval "$2" >/dev/null 2>&1; then printf '  PROBLEM   %-22s %s\n' "$1" "$4"; [ -n "$6" ] || bad=$((bad+1))
  else printf '  ok        %-22s %s\n' "$1" "$(eval "$3" 2>&1 | head -n 1)"; fi
}
bad=0
check 'git' 'git --version' 'git --version' 'installed, but it does not run' 'https://git-scm.com/downloads'
check 'docker' 'docker info' "docker version --format '{{.Server.Version}}'" 'installed, but the Docker daemon is not running: start Docker Desktop (or the docker service)' 'https://docs.docker.com/get-docker/'
check 'kind' 'kind version' 'kind version' 'installed, but it does not run' 'https://kind.sigs.k8s.io/docs/user/quick-start/#installation'
check 'kubectl' 'kubectl version --client' 'kubectl version --client' 'installed, but it does not run' 'https://kubernetes.io/docs/tasks/tools/'
check 'gh' 'gh auth status' 'gh --version' 'installed, but not logged in: run gh auth login' 'https://cli.github.com/'
check 'curl' 'curl --version' 'curl --version' 'installed, but it does not run' 'your package manager (it ships with macOS and most Linux distributions)'
if [ "$bad" -eq 0 ]; then echo "All set: every tool is ready."; else echo "$bad tool(s) need attention. Install or fix them, then run this check again."; fi

3. Clone the reference repo

The finished implementation. You copy from it and compare with it; you never push to it.

mkdir -p ~/platformengr && git clone -q https://github.com/platformengr/kubernetes-platform.git ~/platformengr/kubernetes-platform

Check that it arrived. You should see cluster, tenants, apps, labs and docs:

ls ~/platformengr/kubernetes-platform

4. Create your repo

Your repo of manifests: what should exist in the cluster, one folder per owner. Public, so the lab checks on this site can read it.

cd ~/platformengr && gh repo create YOUR_GITHUB_USER/k8s-platform --public --clone

Go into it and make the folders: cluster for the platform team's cluster files, tenants/checkout for the rules around team-checkout, apps/checkout for what team-checkout runs, and docs.

cd ~/platformengr/k8s-platform && git switch -C main && mkdir -p cluster tenants/checkout apps/checkout docs

5. Create the cluster

Copy the cluster file into your repo, then let kind build the cluster from it. It takes one to two minutes and pulls the node image the first time.

cp ~/platformengr/kubernetes-platform/cluster/kind.yaml ~/platformengr/k8s-platform/cluster/

Create the cluster. Three nodes: one control plane and two workers, each a Docker container.

kind create cluster --config ~/platformengr/k8s-platform/cluster/kind.yaml

kind pointed kubectl at the new cluster. Expect kind-k8s: before you change anything on any cluster, this is the line to check.

kubectl config current-context

Expect three rows with STATUS Ready: k8s-control-plane with ROLES control-plane, and two workers.

kubectl get nodes

A kind node is a container. Expect the same three names:

docker ps --format '{{.Names}}'

6. Find four things in the cluster

Kubernetes runs its own parts as pods in the kube-system namespace. List them with the node each one runs on (-o wide adds the NODE column).

kubectl get pods -n kube-system -o wide

1. kube-apiserver-k8s-control-plane. The only door into the cluster. kubectl, the controllers and the kubelets all talk to it, and to nothing else.

2. etcd-k8s-control-plane. The database where every object is stored. Only the API server talks to it.

3. coredns-..., twice. The cluster's DNS: it turns names like checkout.checkout-dev into addresses. You will need it in module 06.

4. kindnet-..., once per node (three). The pod network. It gives every pod an address and, from module 06, enforces your network rules.

7. Declare the first namespace

team-checkout gets a namespace. The platform team writes it, so it goes under tenants/. The label team: checkout says who it belongs to; later rules select on it.

Create ~/platformengr/k8s-platform/tenants/checkout/namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: checkout-dev
  labels:
    team: checkout

Ask the API server what it would do, without doing it. Expect namespace/checkout-dev created (server dry run): the file is valid and you are allowed.

cd ~/platformengr/k8s-platform && kubectl apply --dry-run=server -f tenants/checkout/namespace.yaml

Create it. kubectl apply sends the file to the API server, which stores the object. Expect namespace/checkout-dev created.

kubectl apply -f tenants/checkout/namespace.yaml

Read its labels. Expect your team=checkout and one you did not write, kubernetes.io/metadata.name=checkout-dev: every namespace gets it, and network rules use it in module 06.

kubectl get namespace checkout-dev --show-labels

8. Commit

Git now says what the cluster has. Push it, so it is on GitHub.

cd ~/platformengr/k8s-platform && git add -A && git commit -qm "Cluster and the checkout-dev namespace" && git push -qu origin main

Simplified for the course

kind runs every node as a container on one machine. A real cluster has machines in several zones, a managed or highly available control plane, and nodes that are replaced, not repaired.

You are cluster-admin and you apply by hand. In a team, nobody applies by hand: a pull request is merged and a controller applies it. That is the Argo CD course.

One namespace per team, called checkout-dev. Real teams get one per environment (checkout-dev, checkout-prod), usually on different clusters.

Versions are pinned: the node image in cluster/kind.yaml (Kubernetes 1.37.0), the ingress controller in cluster/traefik.yaml, the app image in each Deployment. A bump is one commit that changes all of them together.

Break it on purpose: A node stops answering

Your laptop slept, Docker restarted, or (in a real cluster) a machine died. One worker is gone and nobody told Kubernetes in advance.

You practise: reading node status, and finding where a kind node really lives.

1. Break it

docker stop k8s-worker2

2. What you see

Run kubectl get nodes now, and again after a minute. Expected: three nodes Ready. You get: k8s-worker2 turns NotReady after about 40 to 60 seconds. Nothing told you: there is no error, only a status.

Look at kubectl describe node k8s-worker2, under Conditions.

3. Fix it yourself

Bring the node back to Ready without recreating the cluster. Done when kubectl get nodes shows three Ready nodes.

Hint 1

The conditions say Kubelet stopped posting node status. The kubelet runs on the node. Where does a kind node run?

Hint 2

docker ps -a --format '{{.Names}} {{.Status}}': one of the three is Exited.

The fix
docker start k8s-worker2

Give the kubelet half a minute to report in, then expect three Ready nodes:

kubectl get nodes
After you fixed it: the five questions
What failed?
A node: its kubelet stopped reporting to the API server.
How was it detected?
The node controller marks a node NotReady when heartbeats stop. Nobody is paged by that on its own; you saw it because you looked.
Who responds?
The platform team: it owns the nodes.
How is it corrected?
Start the node (here), or replace it (in a real cluster).
How is it prevented?
You cannot stop a node from dying. You make it not matter: more than one replica, spread over nodes (module 04), and an alert on NotReady nodes.

Quiz

  1. What does kubectl apply -f namespace.yaml actually do?
    • It sends the object to the API server, which stores it; controllers then make the cluster match
    • It logs in to a node over the network and runs the commands needed to create the namespace there
    • It copies the file into the cluster, where every kubelet reads it the next time the node restarts
  2. Why does this course put namespaces under tenants/ and Deployments under apps/?
    • Because kubectl applies folders in alphabetical order, and apps/ has to be applied before tenants/
    • Because they have different owners: the platform team sets the rules around a team, the team decides what runs inside
    • Because Kubernetes only accepts Namespace objects from a folder whose name is tenants
  3. A node shows NotReady. What does that tell you?
    • Every pod on that node has crashed and has already been deleted by the scheduler
    • The node is being upgraded on purpose and will come back by itself when it is done
    • The control plane has stopped hearing from that node's kubelet
Answers
  1. It sends the object to the API server, which stores it; controllers then make the cluster match kubectl only talks to the API server; everything else is controllers reacting.
  2. Because they have different owners: the platform team sets the rules around a team, the team decides what runs inside The folder is the ownership line; Kubernetes does not care about folders at all.
  3. The control plane has stopped hearing from that node's kubelet NotReady is about heartbeats, not about the pods.

Field note

The first thing I check on any cluster I am asked to look at is kubectl config current-context. The second is whether what runs there is written down anywhere. If it is not, that is the real problem, whatever the ticket says.

Report a problem with this module