14 KiB
Building our own cluster
-
Let's build our own cluster!
Perfection is attained not when there is nothing left to add, but when there is nothing left to take away. (Antoine de Saint-Exupery)
-
Our goal is to build a minimal cluster allowing us to:
- create a Deployment (with
kubectl runorkubectl create deployment) - expose it with a Service
- connect to that service
- create a Deployment (with
-
"Minimal" here means:
- smaller number of components
- smaller number of command-line flags
- smaller number of configuration files
Non-goals
-
For now, we don't care about security
-
For now, we don't care about scalability
-
For now, we don't care about high availability
-
All we care about is simplicity
Our environment
-
We will use the machine indicated as
dmuc1(this stands for "Dessine Moi Un Cluster" or "Draw Me A Sheep",
in homage to Saint-Exupery's "The Little Prince") -
This machine:
-
runs Ubuntu LTS
-
has Kubernetes, Docker, and etcd binaries installed
-
but nothing is running
-
Checking our environment
- Let's make sure we have everything we need first
.exercise[
-
Log into the
dmuc1machine -
Get root:
sudo -i -
Check available versions:
etcd -version kube-apiserver --version dockerd --version
]
The plan
-
Start API server
-
Interact with it (create Deployment and Service)
-
See what's broken
-
Fix it and go back to step 2 until it works!
Dealing with multiple processes
-
We are going to start many processes
-
Depending on what you're comfortable with, you can:
-
open multiple windows and multiple SSH connections
-
use a terminal multiplexer like screen or tmux
-
put processes in the background with
&
(warning: log output might get confusing to read!)
-
Starting API server
.exercise[
- Try to start the API server:
kube-apiserver # It will fail with "--etcd-servers must be specified"
]
Since the API server stores everything in etcd, it cannot start without it.
Starting etcd
.exercise[
- Try to start etcd:
etcd
]
Success!
Note the last line of output:
serving insecure client requests on 127.0.0.1:2379, this is strongly discouraged!
Sure, that's discouraged. But thanks for telling us the address!
Starting API server (for real)
-
Try again, passing the
--etcd-serversargument -
That argument should be a comma-separated list of URLs
.exercise[
- Start API server:
kube-apiserver --etcd-servers http://127.0.0.1:2379
]
Success!
Interacting with API server
- Let's try a few "classic" commands
.exercise[
-
List nodes:
kubectl get nodes -
List services:
kubectl get services
]
So far, so good.
Note: the API server automatically created the kubernetes service entry.
class: extra-details
What about kubeconfig?
-
We didn't need to create a
kubeconfigfile -
By default, the API server is listening on
localhost:8080(without requiring authentication)
-
By default,
kubectlconnects tolocalhost:8080(without providing authentication)
Creating a Deployment
- Let's run a web server!
.exercise[
- Create a Deployment with NGINX:
kubectl create deployment web --image=nginx
]
Success?
Checking our Deployment status
.exercise[
- Look at pods, deployments, etc.:
kubectl get all
]
Our Deployment is in a bad shape:
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 0/1 0 0 2m26s
And, there is no ReplicaSet, and no Pod.
What's going on?
-
We stored the definition of our Deployment in etcd
(through the API server)
-
But there is no controller to do the rest of the work
-
We need to start the controller manager
Starting the controller manager
.exercise[
- Try to start the controller manager:
kube-controller-manager
]
The final error message is:
invalid configuration: no configuration has been provided
But the logs include another useful piece of information:
Neither --kubeconfig nor --master was specified.
Using the inClusterConfig. This might not work.
Reminder: everyone talks to API server
-
The controller manager needs to connect to the API server
-
It does not have a convenient
localhost:8080default -
We can pass the connection information in two ways:
-
--masterand a host:port combination (easy) -
--kubeconfigand akubeconfigfile
-
-
For simplicity, we'll use the first option
Starting the controller manager (for real)
.exercise[
- Start the controller manager:
kube-controller-manager --master http://localhost:8080
]
Success!
Checking our Deployment status
.exercise[
- Check all our resources again:
kubectl get all
]
We now have a ReplicaSet.
But we still don't have a Pod.
What's going on?
In the controller manager logs, we should see something like this:
E0404 15:46:25.753376 22847 replica_set.go:450] Sync "default/web-5bc9bd5b8d"
failed with `No API token found for service account "default"`, retry after the
token is automatically created and added to the service account
-
The service account
defaultwas automatically added to our Deployment(and to its pods)
-
The service account
defaultexists -
But it doesn't have an associated token
(the token is a secret; creating it requires signature; therefore a CA)
Solving the missing token issue
There are many ways to solve that issue.
We are going to list a few (to get an idea of what's happening behind the scenes).
Of course, we don't need to perform all the solutions mentioned here.
Option 1: disable service accounts
-
Restart the API server with
--disable-admission-plugins=ServiceAccount -
The API server will no longer add a service account automatically
-
Our pods will be created without a service account
Option 2: do not mount the (missing) token
-
Add
automountServiceAccountToken: falseto the Deployment specor
-
Add
automountServiceAccountToken: falseto the default ServiceAccount -
The ReplicaSet controller will no longer create pods referencing the (missing) token
.exercise[
- Programmatically change the
defaultServiceAccount:kubectl patch sa default -p "automountServiceAccountToken: false"
]
Option 3: set up service accounts properly
-
This is the most complex option!
-
Generate a key pair
-
Pass the private key to the controller manager
(to generate and sign tokens)
-
Pass the public key to the API server
(to verify these tokens)
Continuing without service account token
- Once we patch the default service account, the ReplicaSet can create a Pod
.exercise[
- Check that we now have a pod:
kubectl get all
]
Note: we might have to wait a bit for the ReplicaSet controller to retry.
If we're impatient, we can restart the controller manager.
What's next?
-
Our pod exists, but it is in
Pendingstate -
Remember, we don't have a node so far
(
kubectl get nodesshows an empty list) -
We need to:
-
start a container engine
-
start kubelet
-
Starting a container engine
- We're going to use Docker (because it's the default option)
.exercise[
- Start the Docker Engine:
dockerd
]
Success!
Feel free to check that it actually works with e.g.:
docker run alpine echo hello world
Starting kubelet
-
If we start kubelet without arguments, it will start
-
But it will not join the cluster!
-
It will start in standalone mode
-
Just like with the controller manager, we need to tell kubelet where the API server is
-
Alas, kubelet doesn't have a simple
--masteroption -
We have to use
--kubeconfig -
We need to write a
kubeconfigfile for kubelet
Writing a kubeconfig file
-
We can copy/paste a bunch of YAML
-
Or we can generate the file with
kubectl
.exercise[
- Create the file
kubeconfig.kubeletwithkubectl:kubectl --kubeconfig kubeconfig.kubelet config \ set-cluster localhost --server http://localhost:8080 kubectl --kubeconfig kubeconfig.kubelet config \ set-context localhost --cluster localhost kubectl --kubeconfig kubeconfig.kubelet config \ use-context localhost
]
All Kubernetes clients can use kubeconfig
-
The
kubeconfig.kubeletfile has the same format as e.g.~/.kubeconfig -
All Kubernetes clients can use a similar file
-
The
kubectl configcommands can be used to manipulate these files -
This highlights that kubelet is a "normal" client of the API server
Our kubeconfig.kubelet file
The file that we generated looks like the one below.
That one has been slightly simplified (removing extraneous fields), but it is still valid.
apiVersion: v1
kind: Config
current-context: localhost
contexts:
- name: localhost
context:
cluster: localhost
clusters:
- name: localhost
cluster:
server: http://localhost:8080
Starting kubelet
.exercise[
- Start kubelet with that
kubeconfig.kubeletfile:kubelet --kubeconfig kubeconfig.kubelet
]
Success!
Looking at our 1-node cluster
- Let's check that our node registered correctly
.exercise[
- List the nodes in our cluster:
kubectl get nodes
]
Our node should show up.
Its name will be its hostname (it should be dmuc1).
Are we there yet?
- Let's check if our pod is running
.exercise[
- List all resources:
kubectl get all
]
--
Our pod is still Pending. 🤔
--
Which is normal: it needs to be scheduled.
(i.e., something needs to decide on which node it should go.)
Scheduling our pod
-
Why do we need a scheduling decision, since we have only one node?
-
The node might be full, unavailable; the pod might have constraints ...
-
The easiest way to schedule our pod is to start the scheduler
(we could also schedule it manually)
Starting the scheduler
-
The scheduler also needs to know how to connect to the API server
-
Just like for controller manager, we can use
--kubeconfigor--master
.exercise[
- Start the scheduler:
kube-scheduler --master http://localhost:8080
]
- Our pod should now start correctly
Checking the status of our pod
-
Our pod will go through a short
ContainerCreatingphase -
Then it will be
Running
.exercise[
- Check pod status:
kubectl get pods
]
Success!
class: extra-details
Scheduling a pod manually
-
We can schedule a pod in
Pendingstate by creating a Binding, e.g.:kubectl create -f- <<EOF apiVersion: v1 kind: Binding metadata: name: name-of-the-pod target: apiVersion: v1 kind: Node name: name-of-the-node EOF -
This is actually how the scheduler works!
-
It watches pods, takes scheduling decisions, creates Binding objects
Connecting to our pod
- Let's check that our pod correctly runs NGINX
.exercise[
-
Check our pod's IP address:
kubectl get pods -o wide -
Send some HTTP request to the pod:
curl `X.X.X.X`
]
We should see the Welcome to nginx! page.
Exposing our Deployment
- We can now create a Service associated to this Deployment
.exercise[
-
Expose the Deployment's port 80:
kubectl expose deployment web --port=80 -
Check the Service's ClusterIP, and try connecting:
kubectl get service web curl http://`X.X.X.X`
]
--
This won't work. We need kube-proxy to enable internal communication.
Starting kube-proxy
-
kube-proxy also needs to connect to API server
-
It can work with the
--masterflag(even though that will be deprecated in the future)
.exercise[
- Start kube-proxy:
kube-proxy --master http://localhost:8080
]
Connecting to our Service
- Now that kube-proxy is running, we should be able to connect
.exercise[
- Check the Service's ClusterIP again, and retry connecting:
kubectl get service web curl http://`X.X.X.X`
]
Success!
class: extra-details
How kube-proxy works
-
kube-proxy watches Service resources
-
When a Service is created or updated, kube-proxy creates iptables rules
.exercise[
-
Check out the
OUTPUTchain in thenattable:iptables -t nat -L OUTPUT -
Traffic is sent to
KUBE-SERVICES; check that too:iptables -t nat -L KUBE-SERVICES
]
For each Service, there is an entry in that chain.
class: extra-details
Diving into iptables
- The last command showed a chain named
KUBE-SVC-...corresponding to our service
.exercise[
-
Check that
KUBE-SVC-...chain:iptables -t nat -L `KUBE-SVC-...` -
It should show a jump to a
KUBE-SEP-...chains; check it out too:iptables -t nat -L `KUBE-SEP-...`
]
This is a DNAT rule to rewrite the destination address of the connection to our pod.
This is how kube-proxy works!
class: extra-details
kube-router, IPVS
-
With recent versions of Kubernetes, it is possible to tell kube-proxy to use IPVS
-
IPVS is a more powerful load balancing framework
(remember: iptables was primarily designed for firewalling, not load balancing!)
-
It is also possible to replace kube-proxy with kube-router
-
kube-router uses IPVS by default
-
kube-router can also perform other functions
(e.g., we can use it as a CNI plugin to provide pod connectivity)
class: extra-details
What about the kubernetes service?
-
If we try to connect, it won't work
(by default, it should be
10.0.0.1) -
If we look at the Endpoints for this service, we will see one endpoint:
host-address:6443 -
By default, the API server expects to be running directly on the nodes
(it could be as a bare process, or in a container/pod using host network)
-
... And it expects to be listening on port 6443 with TLS