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
sudoen cada servidor. - Conectividad completa entre los tres servidores. En este tutorial se usan sus direcciones IP principales; los ejemplos usan
your_cp_ippara el plano de control yyour_worker1_ipyyour_worker2_ippara los workers. - Un nombre de host distinto en cada servidor (por ejemplo
k8s-cp,k8s-worker1yk8s-worker2). Cámbialo si hace falta consudo 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.
Notalos pasos 1 a 5 se ejecutan en los tres servidores. Los pasos 6 y 7 solo en el plano de control, y el paso 8 en los workers.
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
Consejosi tus servidores tienen una red privada, úsala para el tráfico del clúster. Pasa su IP en
--apiserver-advertise-addressdel paso 6, indica al kubelet de cada nodo que la use añadiendoKUBELET_EXTRA_ARGS=--node-ip=IP_PRIVADAen/etc/default/kubelet, y configura Flannel con el argumento--ifaceapuntando a esa interfaz.
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.
Advertencia
admin.confda control total sobre el clúster. No lo compartas y no lo dejes con permisos de lectura para otros usuarios.
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
NotaFlannel no aplica
NetworkPolicy. Si necesitas políticas de red para aislar aplicaciones, usa Calico o Cilium en su lugar (con su propio procedimiento de instalación y rango de pods).
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
kubectly 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 saveantes de poner cargas importantes.
