Instalando k3d para jugar con k8s.
Tags: kubernetes, docker
Published: 2020-11-01 23:51:16

En este artículo veremos como configurar [k3d](https://k3d.io/) en una máquina virtual con linux, con el objetivo de poder contar con un _mini_ cluster de kubernetes para apredener, practicar o, inclusive, tener aplicaciones corriendo.

K3D es un wrapper livianito sobre 3ks, que justamente la mínima distribución de kubernetes que nos brinda [Rancher Labs](https://rancher.com/). K3D corre sobre Docker, por lo que puede ser instalado sin demasiada complejidad en un host que tenga instalado Docker. Además, K3D nos permite crear "nodos virtuales" que nos sirven para probar algunas de las características de K8s como son __NodeAffinity__, __Taints__ y __Tolerations__.

Otra de las intenciones de este artículo es describir las configuraciones necesarias para poder conectarnos al cluster de kubernetes desde una máquina diferente a la que hostea el cluster, crear un ServiceAccount y darle permisos de administrador y crear otro ServiceAccount con capacidades limitadas.


## Requerimientos Previos
Alcanza con tener una máquina virtual (yo uso [Virtaulbox](https://www.virtualbox.org/)) con ubuntu instalado. La imagen de Ubuntu se puede descargar de este [link](https://www.osboxes.org/virtualbox-images/). La instalación es muy sencilla y hay que tener en cuenta solamente que la máquina virtual debe tener acceso a internet y debe ser "visible" desde nuestra máquina (el host de la máquina virtual).

## Instalando Docker.
Suponiendo que la ip de nuestra máquina virtual es 192.168.0.91, nos logueamos desde el host a la máquina virtual mediante ssh:

```bash
ssh osboxes.org@192.168.0.91
# El password es osboxes.org si bajaste la imagen desde el link que te pasé.
```

luego ejecutamos:

```bash
sudo apt install docker.io
```

configuramos el Docker para que arranque cuando iniciamos la máquina virtual:

```bash
sudo systemctl start docker
sudo systemctl enable docker
```

... finalmente, probamos:

```bash
docker --version
```

si nos muestra la versión, listo... ya tenemos Docker instalado.

podemos ejecutar adicionalmente ;)

```bash
docker run -it --rm busybox echo "Hello from Docker!"
```

## Ahora k3d
Es muy simple la instalación para la última versión estable. Primero necesitamos instalar _curl_:

```bash
sudo apt-get install curl
```

Vamos a necesitar también tener instalado kubectl para poder interactuar con el cluster. Lo instalamos, [acá](https://kubernetes.io/docs/tasks/tools/install-kubectl/) dejo los pasos.

Ya con Kubectl instalado, seguimos...

```bash
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
```

Listo!. Ahora vamos a crear nuestro primer cluster teniendo en cuenta los sigiuentes requerimientos:
* Tenemos que poder crear Persistent Volumes -> necesitamos entonces darle un folder a 3kd para poder crear los volúmenes.
* Tenemos que poder entrar al cluster desde otra máquina (desde el host de la máquina virtual por ejemplo).
* Necesitamos 3 nodos además del master para poder probar algunas cosas.

Bueno, ejecutamos entonces el siguiente comando:

```bash
# Creamos un folder:
mkdir -p /tmp/k3dvol

# Ejecutamos:
k3d cluster create Kluster --api-port 192.168.0.91:6443 --volume /tmp/k3dvol:/tmp/k3dvol -p 8081:80@loadbalancer --agents 3
```
donde:

* __Kluster__ es el nombre del cluster
* La ip __192.168.0.91__ es la ip de la máquina virtual.
* __--agents__ especifica la cantidad de nodos virtuales adicionales al master.
* __-p 8081:80@loadbalancer__ especifica el puerto (8081) a través del cual podremos ingresar al loadbalancer del cluster.

probamos:

```bash
kubectl get nodes

# Deberíamos obtener algo parecido a esto:

NAME                   STATUS   ROLES    AGE   VERSION
k3d-kluster-agent-0    Ready    <none>   12s   v1.18.9+k3s1
k3d-kluster-server-0   Ready    master   10s   v1.18.9+k3s1
k3d-kluster-agent-2    Ready    <none>   11s   v1.18.9+k3s1
k3d-kluster-agent-1    Ready    <none>   10s   v1.18.9+k3s1
```

Muy bien, tenemos un cluster con un nodo maestro y tres nodos esclavos. Vamos a usarlo!

## Accediento al Cluster Desde otra Computadora.
Tanto los sapiens como los no sapiens (pods por ejemplo) pueden comunicarse e interactuar con un cluster de K8s mediante una api. Generalmente los sapiens utilizan __Kubectl__ para hacerlo y el proceso para "conversar" con el cluster implica tres etapas: __Authentication__, __Authorization__ y __Admission Control__. En esta sección veremos entonces como crear un Service Account que nos permita esa conversación con el cluster en forma remota (por fuera del mismo cluster). Vamos a crear un usuario (ServiceAccount) y asignarle permisos de administrador.

#### Paso 1 - Creación del Service Account y Obtención del Secret.

```bash
# Seteamos el usuario a crear (sapiens en este caso):
user=sapien

# Creamos el Service Account en el namespace kube-system:
kubectl create sa ${user} -n kube-system

# Obtenemos el secret para el Service Account creado:
secret=$(kubectl get sa ${user} -n kube-system -o json | jq -r .secrets[].name)
echo "secret = ${secret}"
```

#### Paso 2 - Creación del Certificado y Obtención del Token

```bash
kubectl get secret ${secret} -n kube-system -o json | jq -r '.data["ca.crt"]' | base64 -d > ca.crt
user_token=$(kubectl get secret ${secret} -n kube-system -o json | jq -r '.data["token"]' | base64 -d)
echo "token = ${user_token}"
```

#### Paso 3 - Configuración del Contexto

```bash
c=`kubectl config current-context`
echo "context = $c"

cluster_name=`kubectl config get-contexts $c | awk '{print $3}' | tail -n 1`
echo "cluster_name= ${cluster_name}"

endpoint=`kubectl config view -o jsonpath="{.clusters[?(@.name == \"${cluster_name}\")].cluster.server}"`
echo "endpoint = ${endpoint}"
```

#### Paso 4 - Configuración final del Service Account en el Cluster - Role Binding.

```bash
# Se bindea el service account al rol de administrador existente...
kubectl create clusterrolebinding add-on-cluster-admin-${user} --clusterrole=cluster-admin --serviceaccount=kube-system:${user}

# Creamos el archivo de configuación para la conección.
export KUBECONFIG=$HOME/${user}-kube.yml
touch $HOME/${user}-kube.yml

kubectl config set-cluster ${cluster_name} \
    --embed-certs=true \
    --server=${endpoint} \
    --certificate-authority=./ca.crt

kubectl config set-credentials ${user}-${cluster_name#cluster-} --token=${user_token}

kubectl config set-context ${user}-${cluster_name#cluster-} \
    --cluster=${cluster_name} \
    --user=${user}-${cluster_name#cluster-}
```

Ahora queda copiar el archivo sapiens-kube.yml a la máquina desde la cual nos queremos  conectar a cluster (supongamos el host de la máquina virtual) y agregarlo el path a la variable de entorno __KUBECONFIG__:

```bash
export KUBECONFIG=somepath/sapien-kube.yml
```

Lo probamos:

```bash
#Seteamos el contexto (el nombre del contexto dependerá del nombre del SA):
k config set-context sapiens-k3d-Kluster

k get nodes
...
NAME                   STATUS   ROLES    AGE   VERSION
k3d-kluster-agent-1    Ready    <none>   8h    v1.18.9+k3s1
k3d-kluster-agent-0    Ready    <none>   8h    v1.18.9+k3s1
k3d-kluster-agent-2    Ready    <none>   8h    v1.18.9+k3s1
k3d-kluster-server-0   Ready    master   8h    v1.18.9+k3s1
```

Podemos crear un namespace:

```bash
kubectl create namespace testadmin
...
namespace/testadmin created
```

Bien, accedimos al cluster desde otra máquina a través utilizando un Service Account creado por nostros y bindeado a un rol de adminitrador. Misión cumplida.

## Comó crear un usuario con privilegios limitados?
En la sección anterior vimos como crear un Service Account y cómo bindearlo con un rol con permisos de adminitrador. Ese rol __cluster-admin__ ya estaba creado por defecto por el mismo K8s en su instalación y es un cluster role. Atención con esto, K8s nos proporciona dos tipos de roles, los __Role__ y los __ClusterRole__. Los __ClusterRole__ son roles para operar sobre todo el cluster, son roles cross, mientras que los __ROLE__ sirven para operar dentro de un namespace determinado.

Ahora vamos a ver como crear nosotros un rol con privilegios limitados. Antes, vamos a plantear el siguiente requerimiento: "necesitamos que el usuario amy solamente pueda listar pods en su propio namespace".

Con el requimiento en mente, lo primero que hay que hacer es crear el namespace:

```bash
k create namespace amy
```

El siguiente paso es crear el rol dentro del mismo namespace. Creamos el archivo amy-role.yaml con el siguiente contenido.

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: amy
  name: pod-and-pod-logs-reader
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]
```

lo creamos:

```bash
k create -f amy-role.yaml
...
role.rbac.authorization.k8s.io/pod-and-pod-logs-reader created
```

Ahora tenemos que repetir los mismos haciendo algunos cambios:

```bash
# Seteamos el usuario a crear (amy en este caso):
user=amy

# Creamos el Service Account en el namespace amy:
kubectl create sa ${user} -namespace amy

secret=$(kubectl get sa ${user} -n amy -o json | jq -r .secrets[].name)
echo "secret = ${secret}"

kubectl get secret ${secret} -n amy -o json | jq -r '.data["ca.crt"]' | base64 -d > ca.crt
user_token=$(kubectl get secret ${secret} -n amy -o json | jq -r '.data["token"]' | base64 -d)
echo "token = ${user_token}"


c=`kubectl config current-context`
echo "context = $c"

cluster_name=`kubectl config get-contexts $c | awk '{print $3}' | tail -n 1`
echo "cluster_name= ${cluster_name}"

endpoint=`kubectl config view -o jsonpath="{.clusters[?(@.name == \"${cluster_name}\")].cluster.server}"`
echo "endpoint = ${endpoint}"


# Acá realizamos el binding al role creado unos pasos antes... al rol con privilegios limitados y en el namespace amy.
# Prestar atención: ahora creamos un rolebinding en lugar de un clusterrolebinding y además lo creamos en el namespace amy,
# el mismo namespace en donde creamos el role.
kubectl create rolebinding add-on-cluster-${user} --role=pod-and-pod-logs-reader --serviceaccount=amy:${user} -namespace amy

# Creamos el archivo de configuación para la conección.
export KUBECONFIG=$HOME/${user}-kube.yml
touch $HOME/${user}-kube.yml

kubectl config set-cluster ${cluster_name} \
    --embed-certs=true \
    --server=${endpoint} \
    --certificate-authority=./ca.crt

kubectl config set-credentials ${user}-${cluster_name#cluster-} --token=${user_token}

kubectl config set-context ${user}-${cluster_name#cluster-} \
    --cluster=${cluster_name} \
    --user=${user}-${cluster_name#cluster-}

```
Si todo fue bien, deberíamos tener un archivo __amy-kube.yml__ listo para ser utilizado.

### Vamos a probarlo
Nuevamente, vamos a realizar la prueba desde una máquina diferente a la que tiene corriendo el cluster.

```bash
export KUBECONFIG=somepath/amy-kube.yml
```

Nos paramos en el contexto:

```bash
k config set-context amy-k3d-Kluster --namespace=amy
```

y ejecutamos:

```bash
k get nodes
...
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:amy:amy" cannot list resource "nodes" in API group "" at the cluster scope
```

Perfecto, no nos permitió listar los nodos porque el Service Account amy no tiene permisos. Qué pasa si queremos listar los pods?

```bash
k get pod
...
No resources found in amy namespace.
```
Funciona, nos permite listarlos (aunque no hay ninguno). 

## Comentarios Finales
Hemos visto como instalar de forma muy simple un cluster K3d, cluster que es muy liviano y útil para aprender, realizar pruebas y hasta tenerlo operativo en máquinas con recursos limitados, K3d se monta sobre Docker y es una alternativa interesante.

Si bien en la actualidad existen herramientas con interfaces amigables como [Lens](https://k8slens.dev/) para realizar configuraciones sobre un cluster de kubernetes de forma mucho más simple, entender como funcionan la autenticación y autorización es importante. Este artículo solamente muestra una parte, la parte práctica para crear un Service Account y darle permisos de administrador o permisos limitados sobre un deterinado namespace y permitir operar sobre el cluster 3kd de forma remota. Recomiendo leer más sobre el tema en la página oficial de Kubernetes.