What is Kubernetes
Running one container is easy. Running two hundred of them across a fleet of machines, restarting the ones that die, and adding more when traffic climbs, is a different job entirely. That job is called orchestration, and Kubernetes is what most teams reach for to do it.
What an orchestrator gives you is a way to describe what you want running, and then let something else worry about keeping it that way. Containers get placed on hosts for you. A crashed container comes back without anyone being paged. Load goes up, more replicas appear. Load drops, they go away again.
Kubernetes has won this space fairly decisively, which matters practically: it’s what the tooling targets, what the cloud providers offer as a managed service, and what job descriptions ask for. If you want the full picture, the official Kubernetes documentation is genuinely good. This guide is the hands-on half. Three Ubuntu 18.04 servers, one control plane, two workers, and a cluster you can actually schedule work on by the end.
Installing Docker
Kubernetes doesn’t run containers itself. It schedules them and hands the actual work to a container runtime, so that has to be in place before anything else. Docker is the one this guide uses.
All three servers need it, not just the master. A worker with no runtime will join the cluster and then sit there, unable to start a single pod.
Here are the commands we are going to use to setup docker:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" sudo apt-get update sudo apt-get install -y docker-ce=18.06.1~ce~3-0~ubuntu $ sudo apt-mark hold docker-ce docker-ce set on hold.
Check it took, on every server:
$ sudo docker version Client: Version: 18.06.1-ce API version: 1.38 Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:24:51 2018 OS/Arch: linux/amd64 Experimental: false Server: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:23:15 2018 OS/Arch: linux/amd64 Experimental: false
Installing Kubeadm, Kubelet, and Kubectl
With the runtime sorted, the Kubernetes packages come next. Three of them, and all three go on all three servers including the workers, which catches people out. Here is what each one is for.
Here are the commands used to install the Kubernetes components in this lesson. Run these on all three servers.
Kubeadm: Does the heavy lifting of standing the cluster up and joining nodes to it. Without it you’d be generating certificates and writing manifests by hand.
Kubelet: The agent that actually runs containers on a node. Every server that will run containers needs kubelet, workers included.
Kubectl: The command line client you talk to the cluster with. Strictly it only needs to be where you work, but having it everywhere saves a lot of hopping between shells.
NOTE: There are some issues being reported when installing version 1.12.2-00 from the Kubernetes ubuntu repositories. You can work around this by using version 1.12.7-00 for kubelet, kubeadm, and kubectl. Pinning the version like this is worth doing deliberately rather than taking whatever the repository offers, because a version mismatch between nodes causes problems that are tedious to track down later.
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - OK $ cat << EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list > deb https://apt.kubernetes.io/ kubernetes-xenial main > EOF sudo apt-get update sudo apt-get install -y kubelet=1.15.7-00 kubeadm=1.15.7-00 kubectl=1.15.7-00 sudo apt-mark hold kubelet kubeadm kubectl
Confirm kubeadm is there and working before going any further:
$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.7", GitCommit:"6c143d35bb11d74970e7bc0b6c45b6bfdffc0bd4", GitTreeState:"clean", BuildDate:"2019-12-11T12:40:15Z", GoVersion:"go1.12.12", Compiler:"gc", Platform:"linux/amd64"}
$ systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled)
Drop-In: /etc/systemd/system/kubelet.service.d
└─10-kubeadm.conf
Active: active (running) since Sat 2020-09-12 22:41:06 UTC; 2h 9min ago
Docs: https://kubernetes.io/docs/home/
Main PID: 13315 (kubelet)
Tasks: 17 (limit: 2313)
CGroup: /system.slice/kubelet.service
└─13315 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.co
Making Active the Cluster
Now the cluster itself. The master gets initialised first, which sets up the control plane components, then each worker is joined to it. Nothing here is reversible in a satisfying way, so read the output rather than scrolling past it.
On the Kube master node, initialize the cluster:
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16 I0912 22:40:22.039846 12698 version.go:248] remote version is much newer: v1.19.1; falling back to: stable-1.15 [init] Using Kubernetes version: v1.15.12 [preflight] Running pre-flight checks --- Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 172.31.24.191:6443 --token ei5f3h.ffmlt3ube5qb2b76 \
--discovery-token-ca-cert-hash sha256:45252cd1ebf00694e5b9f0084363702741d6ed6f1bad026b3ef37df393254d39
Give it a few minutes. It pulls the control plane images and waits for them to come up, so a slow connection makes this the longest step in the whole guide. When it finishes, set up the local kubeconfig:
$ mkdir -p $HOME/.kube $ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config $ sudo chown $(id -u):$(id -g) $HOME/.kube/config
That kubeconfig is what tells kubectl where the cluster is and how to authenticate. Skip it and every command afterwards fails with a connection refused, which sends people hunting for a networking problem that doesn’t exist. Check it worked. You should get a Server Version as well as a Client Version, something like this:
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.7", GitCommit:"6c143d35bb11d74970e7bc0b6c45b6bfdffc0bd4", GitTreeState:"clean", BuildDate:"2019-12-11T12:42:56Z", GoVersion:"go1.12.12", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.12", GitCommit:"e2a822d9f3c2fdb5c9bfbe64313cf9f657f0a725", GitTreeState:"clean", BuildDate:"2020-05-06T05:09:48Z", GoVersion:"go1.12.17", Compiler:"gc", Platform:"linux/amd64"}
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady master 8m48s v1.15.7
The kubeadm init command should output a kubeadm join command containing a token and hash. Copy that command and run it with sudo on both worker nodes. It should look something like this:
Worker Node 1
$ sudo kubeadm join 172.31.24.191:6443 --token ei5f3h.ffmlt3ube5qb2b76 \ > --discovery-token-ca-cert-hash sha256:45252cd1ebf00694e5b9f0084363702741d6ed6f1bad026b3ef37df393254d39 [preflight] Running pre-flight checks [preflight] Reading configuration from the cluster... [preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml' [kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.15" ConfigMap in the kube-system namespace [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml" [kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env" [kubelet-start] Activating the kubelet service [kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap... This node has joined the cluster: * Certificate signing request was sent to apiserver and a response was received. * The Kubelet was informed of the new secure connection details. Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
Worker Node 2
$ sudo kubeadm join 172.31.24.191:6443 --token ei5f3h.ffmlt3ube5qb2b76 \ > --discovery-token-ca-cert-hash sha256:45252cd1ebf00694e5b9f0084363702741d6ed6f1bad026b3ef37df393254d39 [preflight] Running pre-flight checks [preflight] Reading configuration from the cluster... [preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml' [kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.15" ConfigMap in the kube-system namespace [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml" [kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env" [kubelet-start] Activating the kubelet service [kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap... This node has joined the cluster: * Certificate signing request was sent to apiserver and a response was received. * The Kubelet was informed of the new secure connection details. Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
Verifying on Master
Back on the master, confirm both workers actually arrived:
$ kubectl get nodes NAME STATUS ROLES AGE VERSION master NotReady master 12m v1.15.7 node1 NotReady <none> 3m1s v1.15.7 node2 NotReady <none> 2m20s v1.15.7
Configuring Networking with Flannel
At this point the cluster exists but it can’t do very much. Pods have no way to talk to each other across nodes, because Kubernetes deliberately leaves networking to a plugin rather than shipping one. Until you install a network add-on, nodes stay NotReady and nothing schedules properly.
Flannel is the one used here. It isn’t the most featureful option, and it’s the least fiddly, which is what you want the first time. You can find more information on Flannel at the project’s home on GitHub: github.com/flannel-io/flannel. The old coreos.com documentation link now redirects somewhere unrelated, since CoreOS was acquired and the project moved.
On all three nodes, run the following:
$ echo "net.bridge.bridge-nf-call-iptables=1" | sudo tee -a /etc/sysctl.conf net.bridge.bridge-nf-call-iptables=1 $ sudo sysctl -p net.bridge.bridge-nf-call-iptables = 1
Install Flannel in the cluster by running this only on the Master node:
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml clusterrole.rbac.authorization.k8s.io/flannel created clusterrolebinding.rbac.authorization.k8s.io/flannel created serviceaccount/flannel created configmap/kube-flannel-cfg created daemonset.extensions/kube-flannel-ds-amd64 created daemonset.extensions/kube-flannel-ds-arm64 created daemonset.extensions/kube-flannel-ds-arm created daemonset.extensions/kube-flannel-ds-ppc64le created daemonset.extensions/kube-flannel-ds-s390x created
Then watch the nodes flip over:
$ kubectl get nodes
You should see all three of your servers listed, and all should have a STATUS of Ready. It should look something like this:
$ kubectl get nodes NAME STATUS ROLES AGE VERSION master Ready master 33m v1.15.7 node1 Ready <none> 24m v1.15.7 node2 Ready <none> 23m v1.15.7
Note: it may take a few moments for all nodes to enter the Ready status, so if they are not all Ready, wait a few moments and try again. That’s normal rather than a sign something’s gone wrong.
Worth checking the Flannel pods themselves too, since a node can report Ready while its networking is still coming up:
$ kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE coredns-5d4dd4b4db-tpqjx 1/1 Running 0 35m coredns-5d4dd4b4db-wk8qs 1/1 Running 0 35m etcd-master 1/1 Running 0 34m kube-apiserver-master 1/1 Running 0 34m kube-controller-manager-master 1/1 Running 0 34m kube-flannel-ds-amd64-5dm6b 1/1 Running 0 2m26s kube-flannel-ds-amd64-cxll8 1/1 Running 0 2m26s kube-flannel-ds-amd64-s8b94 1/1 Running 0 2m26s kube-proxy-fw9fm 1/1 Running 0 35m kube-proxy-ncgsj 1/1 Running 0 25m kube-proxy-swx4g 1/1 Running 0 25m kube-scheduler-master 1/1 Running 0 34m
You should have three pods with flannel in the name, and all three should have a status of Running. If one’s stuck in CrashLoopBackOff, that’s almost always the pod network CIDR not matching what Flannel expects.
Conclusion
That’s a working cluster. Three nodes, all Ready, networking in place, and kubectl talking to it from the master.
What you have built here is a learning cluster rather than a production one. There’s a single control plane node, so losing it loses the cluster. There’s no ingress, no persistent storage configured, and no monitoring. Each of those is a topic in its own right and worth taking one at a time.
Get something scheduled on it first, though. Deploy a pod, watch it land on a worker, delete it and watch it come back. Most of what feels abstract about Kubernetes stops feeling that way once you’ve watched the scheduler actually do its job.


