kubeadm es la herramienta oficial de Kubernetes para crear un clúster en servidores propios: configura el plano de control, genera los certificados y facilita unir más nodos. En este tutorial montarás un clúster con un nodo de plano de control y dos workers sobre Ubuntu 24.04, usando containerd como runtime de contenedores y Flannel como red de pods. Al terminar desplegarás una aplicación de prueba accesible desde fuera del clúster.

Requisitos previos

  • Tres servidores con Ubuntu 24.04 LTS, por ejemplo VPS de CubePath: uno para el plano de control (mínimo 2 vCPU y 2 GB de RAM) y dos workers (mínimo 1 vCPU y 2 GB de RAM, más según tus cargas).
  • Un usuario no root con privilegios sudo en cada servidor.
  • Conectividad completa entre los tres servidores. En este tutorial se usan sus direcciones IP principales; los ejemplos usan your_cp_ip para el plano de control y your_worker1_ip y your_worker2_ip para los workers.
  • Un nombre de host distinto en cada servidor (por ejemplo k8s-cp, k8s-worker1 y k8s-worker2). Cámbialo si hace falta con sudo hostnamectl set-hostname k8s-cp.

Este tutorial usa Kubernetes v1.36. Consulta la última versión menor estable en kubernetes.io/releases y, si es otra, sustituye v1.36 en las URL del paso 4.

Paso 1: Desactivar el swap

Por defecto, el kubelet no arranca si el sistema tiene swap activo, porque interfiere con la gestión de memoria de los pods. Desactívalo ahora y en el arranque:

sudo swapoff -a
sudo sed -i '/\sswap\s/ s/^#*/#/' /etc/fstab

El segundo comando comenta cualquier línea de swap en /etc/fstab. Comprueba que no queda swap:

swapon --show
free -h | grep -i swap

swapon --show no debe devolver nada, y la línea Swap: de free debe mostrar 0B.

Paso 2: Cargar módulos del kernel y ajustar sysctl

Los contenedores y la red de pods necesitan los módulos overlay y br_netfilter, y que el kernel reenvíe paquetes IPv4. Configura los módulos para que se carguen en cada arranque y cárgalos ya:

printf 'overlay\nbr_netfilter\n' | sudo tee /etc/modules-load.d/k8s.conf
sudo modprobe overlay
sudo modprobe br_netfilter

Crea el archivo de parámetros de red:

sudo nano /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1

Aplica la configuración sin reiniciar y verifícala:

sudo sysctl --system
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1

Paso 3: Instalar y configurar containerd

Kubernetes necesita un runtime compatible con CRI. Instalarás containerd desde el repositorio oficial de Docker, que ofrece versiones 2.x actualizadas. No hace falta instalar Docker Engine.

Añade la clave y el repositorio:

sudo apt update
sudo apt install -y ca-certificates curl gpg
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Instala containerd:

sudo apt update
sudo apt install -y containerd.io

El paquete trae una configuración que desactiva el plugin CRI, que es justo lo que usa Kubernetes. Sustitúyela por la configuración por defecto completa y activa el driver de cgroups de systemd, el mismo que usa el kubelet:

containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep SystemdCgroup /etc/containerd/config.toml
            SystemdCgroup = true

Reinicia containerd y comprueba que está activo:

sudo systemctl restart containerd
sudo systemctl enable containerd
systemctl is-active containerd
active

Paso 4: Instalar kubeadm, kubelet y kubectl

Los paquetes de Kubernetes se publican en pkgs.k8s.io, con un repositorio por versión menor. Descarga la clave y añade el repositorio de v1.36:

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/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:/v1.36/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

Instala los tres paquetes y bloquea su versión para que un apt upgrade no los actualice: en Kubernetes las actualizaciones se hacen de forma controlada con kubeadm upgrade.

sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable kubelet

Comprueba las versiones:

kubeadm version -o short
kubelet --version
v1.36.1
Kubernetes v1.36.1

Es normal que en este momento systemctl status kubelet muestre el servicio reiniciándose cada pocos segundos: está esperando la configuración que generará kubeadm.

Paso 5: Abrir el firewall entre los nodos

Los nodos se comunican por varios puertos (API en 6443, etcd en 2379-2380, kubelet en 10250, VXLAN de Flannel en UDP 8472, entre otros). En lugar de abrirlos uno a uno a todo Internet, permite todo el tráfico entre los nodos del clúster y desde la red de pods. En cada servidor ejecuta:

sudo ufw allow OpenSSH
sudo ufw allow from your_cp_ip
sudo ufw allow from your_worker1_ip
sudo ufw allow from your_worker2_ip
sudo ufw allow from 10.244.0.0/16
sudo ufw enable

10.244.0.0/16 es el rango de IP de los pods que usará Flannel. Para gestionar el clúster con kubectl desde tu equipo, abre además la API solo a tu IP en el plano de control:

sudo ufw allow from your_admin_ip to any port 6443 proto tcp

Verifica las reglas:

sudo ufw status

Paso 6: Inicializar el plano de control

Ejecuta este paso solo en el plano de control. kubeadm init comprueba los requisitos, genera certificados, arranca etcd, la API y el resto de componentes como pods estáticos. --pod-network-cidr debe coincidir con el rango que usa Flannel:

sudo kubeadm init --apiserver-advertise-address=your_cp_ip --pod-network-cidr=10.244.0.0/16

Tarda uno o dos minutos. Si termina bien, verás:

Your Kubernetes control-plane has initialized successfully!
...
Then you can join any number of worker nodes by running the following on each as root:

kubeadm join your_cp_ip:6443 --token abcdef.0123456789abcdef \
	--discovery-token-ca-cert-hash sha256:1f2e...

Guarda el comando kubeadm join completo: lo usarás en el paso 8. El token caduca a las 24 horas, pero puedes generar otro cuando quieras.

Configura kubectl para tu usuario copiando el kubeconfig de administrador:

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

Comprueba el estado del nodo:

kubectl get nodes
NAME     STATUS     ROLES           AGE   VERSION
k8s-cp   NotReady   control-plane   45s   v1.36.1

NotReady es lo esperado: falta instalar la red de pods.

Paso 7: Instalar la red de pods Flannel

Kubernetes no incluye red de pods; la aporta un plugin CNI. Flannel es sencillo y suficiente para empezar. Aplica su manifiesto oficial en el plano de control:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Espera a que los pods de Flannel y CoreDNS estén en marcha:

kubectl get pods -n kube-flannel
kubectl get pods -n kube-system -l k8s-app=kube-dns
NAME                    READY   STATUS    RESTARTS   AGE
kube-flannel-ds-7xk2p   1/1     Running   0          40s
NAME                       READY   STATUS    RESTARTS   AGE
coredns-7c65d6cfc9-4bq8n   1/1     Running   0          3m
coredns-7c65d6cfc9-vj9wd   1/1     Running   0          3m

El nodo pasará a Ready:

kubectl get nodes
NAME     STATUS   ROLES           AGE     VERSION
k8s-cp   Ready    control-plane   3m10s   v1.36.1

Paso 8: Unir los nodos worker

En cada worker ejecuta con sudo el comando kubeadm join que guardaste en el paso 6:

sudo kubeadm join your_cp_ip:6443 --token abcdef.0123456789abcdef \
	--discovery-token-ca-cert-hash sha256:1f2e...
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.

Si has perdido el comando o el token ha caducado, genera uno nuevo en el plano de control:

sudo kubeadm token create --print-join-command

De vuelta en el plano de control, comprueba que los tres nodos están listos (puede tardar un minuto mientras Flannel arranca en los workers):

kubectl get nodes -o wide
NAME          STATUS   ROLES           AGE     VERSION   INTERNAL-IP
k8s-cp        Ready    control-plane   12m     v1.36.1   203.0.113.10
k8s-worker1   Ready    <none>          2m      v1.36.1   203.0.113.11
k8s-worker2   Ready    <none>          90s     v1.36.1   203.0.113.12

Paso 9: Desplegar una aplicación de prueba

Crea un Deployment de Nginx con dos réplicas y publícalo con un Service de tipo NodePort, que abre un puerto alto en todos los nodos:

kubectl create deployment hola --image=nginx:stable --replicas=2
kubectl expose deployment hola --port=80 --type=NodePort

Comprueba que las réplicas se reparten entre los workers y mira qué puerto se ha asignado:

kubectl get pods -l app=hola -o wide
kubectl get service hola
NAME   TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hola   NodePort   10.104.51.23   <none>        80:31742/TCP   15s

Desde cualquier nodo, prueba el acceso por el NodePort (sustituye 31742 por el tuyo):

curl -sI http://your_worker1_ip:31742 | head -1
HTTP/1.1 200 OK

Prueba también la resolución DNS interna desde un pod temporal:

kubectl run prueba-dns --rm -it --image=busybox:1.36 --restart=Never -- nslookup hola.default.svc.cluster.local

Debe devolver la CLUSTER-IP del Service. Para acceder desde Internet tendrías que abrir en UFW el rango NodePort (sudo ufw allow 30000:32767/tcp); en producción es más habitual poner un Ingress Controller delante. Elimina la prueba:

kubectl delete service hola
kubectl delete deployment hola

Solución de problemas

kubeadm init falla en las comprobaciones previas. Lee el mensaje [ERROR ...]: los habituales son swap activo (paso 1), /proc/sys/net/ipv4/ip_forward distinto de 1 (paso 2) o que el runtime no responde (paso 3). Corrige la causa en lugar de usar --ignore-preflight-errors.

El kubelet no arranca después de kubeadm init o join. Revisa su log:

sudo journalctl -u kubelet -n 50 --no-pager

Un error sobre el driver de cgroups indica que SystemdCgroup no está a true en containerd; corrígelo y reinicia containerd y kubelet.

Un nodo se queda en NotReady. Comprueba que el pod de Flannel de ese nodo está Running con kubectl get pods -n kube-flannel -o wide y mira sus logs con kubectl logs -n kube-flannel <pod>. La causa más común es el firewall: el tráfico UDP 8472 entre nodos está bloqueado.

Los pods de un nodo no alcanzan a los de otro, o CoreDNS no responde. Otra vez suele ser el firewall o que los nodos se anuncian con IP que no se ven entre sí. Revisa la columna INTERNAL-IP de kubectl get nodes -o wide.

Empezar de cero en un nodo. kubeadm reset deshace lo que hicieron init o join. Después borra la configuración de red de CNI y el kubeconfig:

sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d "$HOME/.kube"

Si el nodo era un worker, elimínalo también del clúster desde el plano de control con kubectl delete node k8s-worker1.

Conclusión

Tienes un clúster de Kubernetes funcional con un plano de control, dos workers, containerd como runtime y Flannel como red de pods, instalado desde los repositorios oficiales y con las versiones bloqueadas. Es una buena base para aprender y para cargas que no necesiten alta disponibilidad del plano de control.

Como siguientes pasos puedes:

  • Aprender a gestionar Pods, Deployments y Services con kubectl y manifiestos YAML.
  • Instalar un Ingress Controller y cert-manager para publicar aplicaciones con HTTPS.
  • Añadir almacenamiento persistente y copias de seguridad de etcd con etcdctl snapshot save antes de poner cargas importantes.