Skip to main content

Kubernetes

Kubernetes es un gestor de contenedores que ofrece varios servicios para su funcionamiento. A muy grandes rasgos podemos decir que tenemos tres bloques: motor de contenedores, gestor contenedores y red. Kubernetes es el gestor de contenedores.

La lista oficial indica qué motores admite. De estos me he decantado por cri-o, ya que su conectividad con Kubernetes es bastante buena y puedes instalar únicamente las piezas básicas para este, aun así, si en el futuro hiciera falta, se puede instalar las herramientas de gestión para trabajar con él directamente.

Seleccionar la red ha sido más complicado ya que es una pieza clave y hay muchas diferencias. Me he decantado por Calico debido a su antigüedad y la gran comunidad que tiene detrás.

Con las decisiones tomadas, se inicia la instalación.

tip

Los siguientes pasos hay que realizarlos en el maestro y en todos los nodos a no ser que indiquen lo contrario

Desactivar SWAP

Aunque Kubernetes ha empezado a aceptar el uso de swap, no es recomendable, así que lo primero será desactivarlo.

sudo swapoff -a
sudo vi /etc/fstab

Borramos la línea de swap y eliminamos el fichero para ahorrar espacio.

sudo rm /swap.img

Configurar la red

Tanto Kubernetes como cri-o requieren algunos cambios en la red para las comunicaciones entre el clúster.

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

sudo tee /etc/sysctl.d/99-kubernetes-cri.conf<<EOF
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

sudo sysctl --system

Instalar Kubernetes

Necesitamos añadir los repositorios oficiales.

sudo curl https://packages.cloud.google.com/apt/doc/apt-key.gpg -o /etc/apt/trusted.gpg.d/kubernetes.gpg
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update

Instalamos y bloqueamos los componentes para evitar actualizar sin querer. La actualización de Kubernetes requiere de muchos procedimientos, actualizar un solo trozo hará que falle todo el sistema.

sudo apt install kubelet kubeadm kubectl -y
sudo apt-mark hold kubelet kubeadm kubectl

Revisamos la versión ya que nos hará falta en el siguiente punto

kubeadm version
tip

El proceso siempre instala la última versión. Podemos elegir una concreta si lo necesitamos, por ejemplo:

sudo apt install kubelet=1.24.4-00 kubeadm=1.24.4-00 kubectl=1.24.4-00 -y

Instalar cri-o

warning

Kubernetes y cri-o deben tener exactamente la misma versión. Si tienes mala suerte como me ha pasado varias veces (¡gracias, Murphy! ¬¬'), Kubernetes habrá liberado una versión superior a la de cri-o, así que se deberá eliminar Kubernetes e instalar la última compatible con cri-o.

Definimos las variables acordes a la versión de Ubuntu y de Kubernetes.

OS_VERSION=xUbuntu_22.04
CRIO_VERSION=1.25

Añadimos los repositorios.

curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS_VERSION/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS_VERSION/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-crio-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS_VERSION/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
echo "deb [signed-by=/usr/share/keyrings/libcontainers-crio-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS_VERSION/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION.list
sudo apt update

Instalamos e iniciamos el servicio

sudo apt install -y cri-o cri-o-runc
sudo systemctl daemon-reload
sudo systemctl enable crio --now
info

Debido a que el repositorio de cri-o apunta a una versión concreta, no necesitamos bloquear los componentes, nunca se actualizará por error.

Iniciar el clúster

compte

Este proceso solo se debe realizar en el maestro.

Iniciamos el servicio y descargamos las imágenes.

sudo systemctl enable kubelet
sudo kubeadm config images pull
tip

El sistema detecta qué motor tenemos instalado, en caso de tener múltiples deberíamos indicar cuál utilizar.

Iniciamos el clúster con la red por defecto.

sudo kubeadm init --control-plane-endpoint master.domain.intranet
warning

Si no hemos instalado la última versión de Kubernetes, deberemos indicar qué versión debe desplegar. Por ejemplo:

sudo kubeadm config images pull --kubernetes-version 1.24.0
sudo kubeadm init --kubernetes-version 1.24.0

Añadimos los ficheros de acceso a Kubernetes para nuestro usuario.

sudo mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Desplegamos Calico.

kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

Añadir nodos

Cada nodo debe ser incluido en el clúster. Cuando se inicializa nos indica el comando a ejecutar, pero podemos volver a mostrarlo.

kubeadm token create --print-join-command
compte

El comando que nos mostrará no tendrá el sudo al inicio.

Tendremos algo como el siguiente que ejecutaremos en cada nodo:

sudo kubeadm join <ip>:6443 --token <token> --discovery-token-ca-cert-hash sha256:<sha>

Comprobamos que los nodos han sido añadidos.

kubectl get nodes

Reiniciamos el servicio de resolución de DNS.

kubectl -n kube-system rollout restart deployment coredns
tip

En muchas versiones de Kubernetes me he encontrado que la resolución de dominios no funcionaba. Reiniciar el servicio coredns lo resuelve.

Esperamos a que todos los pods se desplieguen para terminar.

watch kubectl get pods -n kube-system

Maestro como nodo

warning

Esto no es recomendable en entornos de producción.

En redes pequeñas quizás no tengamos suficientes nodos, así que puede ser útil habilitar el Máster como otro nodo más.

kubectl taint node Master node-role.kubernetes.io/control-plane:NoSchedule-
compte

Actualizar el Master del comando acorde al nombre del servidor maestro.

Pruebas

La experiencia me ha dado malas pasadas, así que prefiero siempre confirmar que todo está funcionando según lo esperado.

Desplegamos el servicio para comprobar las DNS.

kubectl apply -f https://k8s.io/examples/admin/dns/dnsutils.yaml

Una vez desplegado, comprobamos que se resuelven los dominios del clúster, de la red interna y de internet.

kubectl exec -i -t dnsutils -- nslookup kubernetes.default
kubectl exec -i -t dnsutils -- nslookup Node1
kubectl exec -i -t dnsutils -- nslookup www.google.com
compte

Node1 se deberá actualizar según el nombre de algún servidor o servicio en la red.

Si todo ha funcionado bien, borramos el servicio.

kubectl delete -f https://k8s.io/examples/admin/dns/dnsutils.yaml

Cambiar registro de contenedores

perill

En la versión 1.29 ha cambiado de fichero.

El motor de contenedores nos permite elegir en qué orden debe buscar las imágenes que queremos desplegar, saltando entre repositorios hasta que la encuentre.

En el momento en que nuestra red dispone de un repositorio propio como el de Nexus, se debe aplicar este cambio el cual nos aporta:

  • Podremos desplegar contenedores que únicamente están en nuestro repositorio, como contenedores de uso interno, pruebas o aún en desarrollo.
  • Este repositorio puede actuar de espejo de otros como Docker Hub o quay.io, evitando así las restricciones de estos y acelerando los tiempos de descarga.

En todos los nodos debemos cambiar la configuración.

sudo vi /etc/containers/registries.conf

Añadimos nuestro repositorio como el primero en la lista y desactivamos la comprobación del certificado ya que, al estar en la intranet, su certificado no será válido.

/etc/containers/registries.conf
unqualified-search-registries = ["registry.domain.intranet", "docker.io", "quay.io"]

[[registry]]
location="registry.domain.intranet"
insecure=true

Tras realizar se aplicarán los cambios.

sudo systemctl restart cri-o.service

Usuarios de sistema

Tras desplegar Kubernetes tendremos el fichero config que nos permite conectar con el usuario administrador, teniendo acceso a todo el sistema. Los usuarios o servicios que requieran conectar al sistema deberán hacerlo con un usuario específico para ello, con acceso limitado.

Estos usuarios suelen estar limitados dentro de un namespace, en ese caso deberían crearse dentro de él. Para los usuarios con acceso a más de un namespace, tenemos la opción de crearlos en un namespace únicamente para almacenar estos usuarios, en uno de los namespace que tenga acceso o en kube-system, dependerá de cómo prefiramos organizarlos.

Se debe crear el usuario, el rol y la unión entre ambos. Para cada usuario los roles necesarios serán muy diferentes, así que dejo el ejemplo para un caso típico: un usuario con acceso total únicamente a un namespace.

Creamos el namespace y el usuario.

kubectl create namespace project1
kubectl create serviceaccount project1-admin -n project1

Creamos un fichero con los roles.

vi role-admin-project1.yaml

Creamos el rol con todos los permisos dentro del namespace.

role-admin-project1.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: admin
namespace: project1
rules:
- apiGroups: ["", "extensions", "apps", "networking.k8s.io"]
resources: ["*"]
verbs: ["*"]
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["*"]
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["*"]

Preparamos la unión.

vi role-binding-project1-admin-project1.yaml

Asignamos el usuario y rol previamente definidos.

role-binding-project1-admin-project1.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: admin
namespace: project1
subjects:
- kind: ServiceAccount
name: project1-admin
namespace: project1
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: admin

Aplicamos tanto el rol como la unión con el usuario.

kubectl apply -f role-admin-project1.yaml
kubectl apply -f role-binding-project1-admin-project1.yaml

Conexión remota

Muchas veces necesitaremos poder utilizar los usuarios de forma remota, ya sea desde una consola administrativa o desde comandos. Para ello podemos requerir de tokens o de un fichero de configuración, comúnmente llamado kubeconfig.

tip

Desde Kubernetes 1.24, los usuarios de sistema no generan tokens de acceso por defecto, así que se deben crear manualmente cuando sea necesario.

Si lo único que necesitamos es un token acorde al usuario, podemos crear uno asignando qué duración tendrá.

kubectl create token project1-admin -n project1 --duration=5h

La mayoría de los casos requerirá disponer del kubeconfig. Este es algo más complejo de crear. Primero debemos preparar las variables.

compte

Este ejemplo genera un token de duración ilimitada.

USER=project1-admin
NAMESPACE=project1
CURRENT_CONTEXT=`kubectl config current-context`
CLUSTER_ENDPOINT=`kubectl config view -o "jsonpath={.clusters[0].cluster.server}"`
CLUSTER_CA_CERT=`kubectl config view --raw -o "jsonpath={.clusters[0].cluster.certificate-authority-data}"`
SA_SECRET_TOKEN=`kubectl create token $USER -n $NAMESPACE --duration=999999h`

Creamos el fichero de configuración que deberemos enviar a los usuarios o servicios que lo requieran.

cat << EOF > $USER-config
apiVersion: v1
kind: Config
current-context: ${CURRENT_CONTEXT}
contexts:
- name: ${CURRENT_CONTEXT}
context:
cluster: ${CURRENT_CONTEXT}
user: ${USER}
clusters:
- name: ${CURRENT_CONTEXT}
cluster:
certificate-authority-data: ${CLUSTER_CA_CERT}
server: ${CLUSTER_ENDPOINT}
users:
- name: ${USER}
user:
token: ${SA_SECRET_TOKEN}
EOF
warning

Este proceso de creación es simplista acorde a un Kubernetes según el explicado en estos artículos. En uno mayor será necesario retocar algunas secciones.

Actualización

Kubernetes tiene un roadmap medianamente rápido, así que se debe mantener, siempre que sea posible, actualizado.

En cada actualización, varias partes pasan a estar obsoletas o dejan de existir, así que puede que alguno de los servicios actuales deje de funcionar. Lo recomendable es seguir estos pasos:

  • Hacer copias de seguridad de todo lo necesario. Si es posible, utilizar snapshots.
  • Antes de actualizar Kubernetes, actualizar todos los servicios en este. Esta regla no siempre es viable ya que un servicio nuevo puede no ser compatible con nuestra versión vieja de Kubernetes.

Sistema operativo

Antes de nada, actualizamos el sistema operativo.

sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y
tip

Actualizaremos los repositorios de Kubernetes y CRI-O, así que si estos están obsoletos los podemos borrar en este paso.

Kubernetes + CRI-O (≤ 1.28)

Antes de empezar deben quedar claros varios temas:

  • Kubernetes utiliza las versiones semánticas, acorde al patrón: mayor.menor.parche.
  • Se debe actualizar a la siguiente versión menor. Por ejemplo, de la 1.25 a 1.26, después a 1.27.
  • Utilizar siempre la versión con el último parche.
  • CRI-O debe tener la misma versión mayor y menor que kubelet y se debe actualizar antes que este.
  • La actualización de kubelet requiere drenar los servicios del nodo. Estos son movidos al resto de nodos, si no hay espacio no se iniciarán.
  • Si un servicio requiere 3 nodos de tipo worker (como Longhorn), puede quedar inactivo hasta que el nodo vuelva a estar disponible.
  • Los nodos maestros (control-plane) se actualizan antes que los nodos de tipo worker.
  • Los pasos para seguir son los mismos sin importar el tipo de nodo, a no ser que un paso indique lo contrario.

Actualizamos el repositorio de Kubernetes

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update
info

Este ejemplo es para saltar de 1.25.X a 1.26.X. Actualizar y repetir el mismo proceso acorde al salto que sea necesario.

Buscamos el último parche de la versión menor de destino.

apt-cache madison kubeadm | grep 1.26

Con esa información, desbloqueamos, actualizamos y volvemos a bloquear el kubeadm.

sudo apt-mark unhold kubeadm
sudo apt-get install -y kubeadm=1.26.7-00
sudo apt-mark hold kubeadm
perill

Aunque instalamos el kubeadm en todos los nodos, únicamente se debe lanzar el actualizador en los control-plane. Calico solo se actualiza una vez.

Lanzamos el validador para que nos indique las opciones a las que podemos actualizar.

sudo kubeadm upgrade plan

Elegimos la más nueva.

sudo kubeadm upgrade apply v1.26.7

En algunos casos es posible que necesitemos actualizar la network de Calico.

kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

Liberamos el trabajo del nodo que vamos a actualizar. Esta orden se lanza en el master.

kubectl drain <nodeName> --ignore-daemonsets
perill

Este comando requerirá añadir --delete-emptydir-data por Longhorn u otros servicios que utilizan disco local.

Añadimos el repositorio indicando de CRIO_VERSION la misma que el destino.

OS_VERSION=xUbuntu_22.04
CRIO_VERSION=1.26
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS_VERSION/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS_VERSION/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-crio-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS_VERSION/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
echo "deb [signed-by=/usr/share/keyrings/libcontainers-crio-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS_VERSION/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION.list
sudo apt update

Actualizamos e iniciamos CRI-O.

sudo apt install -y cri-o cri-o-runc
sudo systemctl daemon-reload
sudo systemctl enable crio --now

Actualizamos e iniciamos el resto de los elementos.

sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet=1.26.7-00 kubectl=1.26.7-00
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet
perill

Prefiero reiniciar el sistema operativo antes de volver a liberarlo para evitar sorpresas.

Reiniciamos el nodo.

sudo init 6

Con el nodo actualizado, lo liberamos para aceptar trabajo. Esta orden se lanza en el master.

kubectl uncordon <nodeName>

Kubernetes + CRI-O (≥ 1.29)

A partir de la versión 1.29, CRI-O se ha añadido al repositorio oficial de Kubernetes, modificando la forma de sincronizarse con este, con lo que el proceso es ligeramente diferente.

Antes de nada hay que revisar las versiones de Kubernetes y cuáles de ellas son compatibles con CRI-O ya que ambos deben estar en la misma versión siendo los 2 primeros digitos los que deben coincidir.

Ya no es necesario bloquear las actualizaciones de ningún componente ya que cada versión está en un repositorio diferente.

sudo apt-mark unhold kubeadm kubelet kubectl
tip

El unhold solo será necesario la primera vez, a las siguientes no estarán bloqueados y podemos saltar ese comando.

Borrar los viejos repositorios.

sudo rm /etc/apt/sources.list.d/*
sudo rm /etc/apt/keyrings/*

Actualizar la variable acorde a la siguiente versión.

KUBERNETES_VERSION=v1.29
# CRI-O repository
curl -fsSL https://pkgs.k8s.io/addons:/cri-o:/stable:/$KUBERNETES_VERSION/deb/Release.key |
sudo gpg --dearmor -o /etc/apt/keyrings/cri-o-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/cri-o-apt-keyring.gpg] https://pkgs.k8s.io/addons:/cri-o:/stable:/$KUBERNETES_VERSION/deb/ /" |
sudo tee /etc/apt/sources.list.d/cri-o.list

# Kubernetes repository
curl -fsSL https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/deb/Release.key |
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/deb/ /" |
sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update

Únicamente el master debe actualizar el sistema.

sudo apt-get install -y kubeadm
sudo kubeadm upgrade plan

El plan nos dará la versión que debemos aplicar.

sudo kubeadm upgrade apply v1.29.4
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
perill

NUNCA actualizar cri-o y resto en el master hasta que este termine de actualizar el sistema.

Actualizamos todos los servicios y reiniciamos el Linux

sudo apt-get install -y cri-o kubelet kubeadm kubectl
sudo init 6
info

La actualización de los nodos puede seguir realizándose nodo a nodo para no tener tiempos inoperativo, pero ojo porque hay servicios (como Longhorn) que requieren un número mínimo de nodos para funcionar, si se elimina un nodo por debajo de ese mínimo, estos dejan de funcionar correctamente causando problemas superiores a apagar todo, actualizar y reiniciar.

kubectl drain <nodeName> --ignore-daemonsets --delete-emptydir-data
kubectl uncordon <nodeName>

Errores conocidos

Cuando un nodo no responde, se debe revisar los últimos logs en ese nodo.

journalctl -n 20 -u kubelet

1.27

Se ha eliminado la opción container-runtime, provocando un error en todos los nodos workers cuando son actualizados a esta versión.

Editar el siguiente fichero:

sudo vi /var/lib/kubelet/kubeadm-flags.env

Eliminar --container-runtime=remote y reiniciar el `kubelet``.

1.29

La lista de repositorios de imágenes se ha movido a otro fichero. La actualización no migra estos datos, así que, en caso de utilizarlo, se debe actualizar.

sudo vi /etc/containers/registries.conf.d/crio.conf

Este es un ejemplo para un repositorio propio.

/etc/containers/registries.conf.d/crio.conf
unqualified-search-registries = ["registry.domain.intranet", "docker.io", "quay.io"]
[[registry]]
location="registry.domain.intranet"
insecure=true

Reiniciar para leer la configuración.

sudo systemctl restart cri-o.service