mirror of
https://github.com/jpetazzo/container.training.git
synced 2026-05-05 00:16:39 +00:00
Add Dessine-Moi Un Cluster
This commit is contained in:
837
slides/k8s/dmuc.md
Normal file
837
slides/k8s/dmuc.md
Normal file
@@ -0,0 +1,837 @@
|
||||
# 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 run` or `kubectl create deployment`)
|
||||
- expose it with a Service
|
||||
- connect to that service
|
||||
|
||||
|
||||
- "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 `dmuc`
|
||||
|
||||
(this stands for "Dessine Moi Un Cluster" or "Draw Me A Sheep",
|
||||
<br/>in hommage 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 `dmuc` machine
|
||||
|
||||
- Get root:
|
||||
```bash
|
||||
sudo -i
|
||||
```
|
||||
|
||||
- Check available versions:
|
||||
```bash
|
||||
etcd -version
|
||||
kube-apiserver --version
|
||||
dockerd --version
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## The plan
|
||||
|
||||
1. Start API server
|
||||
|
||||
2. Interact with it (create Deployment and Service)
|
||||
|
||||
3. See what's broken
|
||||
|
||||
4. 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 `&`
|
||||
<br/>(warning: log output might get confusing to read!)
|
||||
|
||||
---
|
||||
|
||||
## Starting API server
|
||||
|
||||
.exercise[
|
||||
|
||||
- Try to start the API server:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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-servers` argument
|
||||
|
||||
- That argument should be a comma-separated list of URLs
|
||||
|
||||
.exercise[
|
||||
|
||||
- Start API server:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
- List services:
|
||||
```bash
|
||||
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 `kubeconfig` file
|
||||
|
||||
- By default, the API server is listening on `localhost:8080`
|
||||
|
||||
(without requiring authentication)
|
||||
|
||||
- By default, `kubectl` connects to `localhost:8080`
|
||||
|
||||
(without providing authentication)
|
||||
|
||||
---
|
||||
|
||||
## Creating a Deployment
|
||||
|
||||
- Let's run a web server!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a Deployment with NGINX:
|
||||
```bash
|
||||
kubectl create deployment web --image=nginx
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Success?
|
||||
|
||||
---
|
||||
|
||||
## Checking our Deployment status
|
||||
|
||||
.exercise[
|
||||
|
||||
- Look at pods, deployments, etc.:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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:8080` default
|
||||
|
||||
- We can pass the connection information in two ways:
|
||||
|
||||
- `--master` and a host:port combination (easy)
|
||||
|
||||
- `--kubeconfig` and a `kubeconfig` file
|
||||
|
||||
- For simplicity, we'll use the first option
|
||||
|
||||
---
|
||||
|
||||
## Starting the controller manager (for real)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Start the controller manager:
|
||||
```bash
|
||||
kube-controller-manager --master http://localhost:8080
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Success!
|
||||
|
||||
---
|
||||
|
||||
## Checking our Deployment status
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check all our resources again:
|
||||
```bash
|
||||
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 `default` was automatically added to our Deployment
|
||||
|
||||
(and to its pods)
|
||||
|
||||
- The service account `default` exists
|
||||
|
||||
- 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: false` to the Deployment spec
|
||||
|
||||
*or*
|
||||
|
||||
- Add `automountServiceAccountToken: false` to the default ServiceAccount
|
||||
|
||||
- The ReplicaSet controller will no longer create pods referencing the (missing) token
|
||||
|
||||
.exercise[
|
||||
|
||||
- Programmatically change the `default` ServiceAccount:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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 `Pending` state
|
||||
|
||||
- Remember, we don't have a node so far
|
||||
|
||||
(`kubectl get nodes` shows 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:
|
||||
```bash
|
||||
dockerd
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Success!
|
||||
|
||||
Feel free to check that it actually works with e.g.:
|
||||
```bash
|
||||
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 controler manager, we need to tell kubelet where is the API server
|
||||
|
||||
- Alas, kubelet doesn't have a simple `--master` option
|
||||
|
||||
- We have to use `--kubeconfig`
|
||||
|
||||
- We need to write a `kubeconfig` file 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.kubelet` with `kubectl`:
|
||||
```bash
|
||||
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.kubelet` file has the same format as e.g. `~/.kubeconfig`
|
||||
|
||||
- All Kubernetes clients can use a similar file
|
||||
|
||||
- The `kubectl config` commands 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.
|
||||
|
||||
```yaml
|
||||
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.kubelet` file:
|
||||
```bash
|
||||
kubelet --kubeconfig kubelet.kubeconfig
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Success!
|
||||
|
||||
---
|
||||
|
||||
## Looking at our 1-node cluster
|
||||
|
||||
- Let's check that our node registered correctly
|
||||
|
||||
.exercise[
|
||||
|
||||
- List the nodes in our cluster:
|
||||
```bash
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Our node should show up.
|
||||
|
||||
Its name will be its hostname (it should be `dmuc`).
|
||||
|
||||
---
|
||||
|
||||
## Are we there yet?
|
||||
|
||||
- Let's check if our pod is running
|
||||
|
||||
.exercise[
|
||||
|
||||
- List all resources:
|
||||
```bash
|
||||
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 `--kubeconfig` or `--master`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Start the scheduler:
|
||||
```bash
|
||||
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 `ContainerCreating` phase
|
||||
|
||||
- Then it will be `Running`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check pod status:
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Success!
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Scheduling a pod manually
|
||||
|
||||
- We can schedule a pod in `Pending` state by creating a Binding, e.g.:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
kubectl get pods -o wide
|
||||
```
|
||||
|
||||
- Send some HTTP request to the pod:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
kubectl expose deployment web --port=80
|
||||
```
|
||||
|
||||
- Check the Service's ClusterIP, and try connecting:
|
||||
```bash
|
||||
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 `--master` flag
|
||||
|
||||
(even though that will be deprecated in the future)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Start kube-proxy:
|
||||
```bash
|
||||
kube-proxy --master http://localhost:8080
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Connecting to our Service
|
||||
|
||||
- Now that kube-proxy is running, we should be able to connect
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check again the Service's ClusterIP, and retry connecting:
|
||||
```bash
|
||||
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 `OUTPUT` chain in the `nat` table:
|
||||
```bash
|
||||
iptables -t nat -L OUTPUT
|
||||
```
|
||||
|
||||
- Traffic is sent to `KUBE-SERVICES`, check that too:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
iptables -t nat -L `KUBE-SVC-...`
|
||||
```
|
||||
|
||||
- It should show a jump to a `KUBE-SEP-...` chains; check it out too:
|
||||
```bash
|
||||
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-router 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
|
||||
Reference in New Issue
Block a user