En la lección anterior levantamos clústeres desechables en el portátil y descubrimos, casi de pasada, que kind usa kubeadm por dentro: aquel campo kubeadmConfigPatches del fichero de configuración no era casualidad. Ahora vamos a la herramienta directamente.

kubeadm es la herramienta oficial del proyecto Kubernetes para arrancar un clúster conforme sobre máquinas que tú controlas. Es la base sobre la que están construidas casi todas las distribuciones, y entenderla te da algo que ninguna otra vía te da: ver con tus propios ojos dónde están los certificados, dónde viven los pods estáticos del plano de control y qué ocurre exactamente cuando un nodo se une al clúster. Aunque Rutas Norte S.L. acabe eligiendo un clúster gestionado (10-06), este conocimiento es el que te permitirá depurar un plano de control roto y aprobar la certificación CKA (12-01).

Contenido

  1. Qué es kubeadm y qué no es
  2. Autogestionado frente a gestionado: cuándo tiene sentido
  3. Preparación de las máquinas
  4. Instalación de containerd
  5. Instalación de kubeadm, kubelet y kubectl
  6. kubeadm init con fichero de configuración
  7. Qué crea exactamente kubeadm en el nodo
  8. Instalar el CNI y por qué los nodos están NotReady
  9. Unir nodos de trabajo
  10. Plano de control en alta disponibilidad
  11. Gestión de certificados
  12. Actualización de versión
  13. Copia de seguridad y restauración de etcd
  14. kubeadm reset y limpieza
  15. Errores comunes y consejos
  16. Ejercicios
  17. Conclusión

  1. Qué es kubeadm y qué no es

kubeadm hace una sola cosa bien: dadas unas máquinas ya preparadas, arranca en ellas un plano de control y une nodos de trabajo, produciendo un clúster que pasa las pruebas de conformidad de Kubernetes.

kubeadm SÍ hace kubeadm NO hace
Generar toda la jerarquía de certificados (CA, apiserver, etcd, kubelet) Aprovisionar máquinas virtuales o físicas
Escribir los manifiestos de los pods estáticos del plano de control Instalar el runtime de contenedores
Arrancar etcd (apilado) o conectarse a uno externo Instalar el complemento de red (CNI)
Configurar el kubelet y generar sus kubeconfig Configurar el sistema operativo (swap, sysctl, cortafuegos)
Emitir tokens de unión y unir nodos Instalar Ingress, monitorización, almacenamiento
Actualizar el plano de control de versión Gestionar copias de seguridad de etcd
Renovar certificados Ofrecer alta disponibilidad del balanceador de la API

Esa lista de la derecha es la clave. Mucha gente ejecuta kubeadm init, ve el mensaje de éxito, y luego se sorprende de que kubectl get nodes diga NotReady. No hay ningún error: falta el CNI, y ese trabajo no es de kubeadm.

flowchart TB
    subgraph tuyo["Responsabilidad tuya"]
        A[Máquinas y red]
        B[Sistema operativo:<br/>swap, sysctl, cortafuegos]
        C[containerd]
        E[CNI: Calico, Cilium...]
        F[Complementos: Ingress,<br/>almacenamiento, monitorización]
        G[Copias de etcd, parches,<br/>rotación de nodos]
    end
    subgraph kubeadm["Responsabilidad de kubeadm"]
        D[Certificados, plano de control,<br/>tokens, unión de nodos,<br/>actualizaciones]
    end
    A --> B --> C --> D --> E --> F --> G
    style kubeadm fill:#e8f4ff
    style tuyo fill:#fff4e8

  1. Autogestionado frente a gestionado: cuándo tiene sentido

Antes de escribir una sola orden, la pregunta honesta: ¿debería Rutas Norte S.L. operar su propio clúster?

Criterio kubeadm (autogestionado) Gestionado (10-06)
Coste del plano de control Tus máquinas (3 nodos de control mínimo para HA) 0-75 €/mes por clúster, según proveedor
Quién arregla un etcd corrupto a las 3 de la mañana Tu equipo El proveedor
Actualizaciones de versión Manuales, nodo a nodo, con ventana de mantenimiento Un botón, aunque sigue exigiendo planificación
Control sobre banderas del apiserver Total Limitado
Ejecutar en máquinas propias o en un centro de datos privado No (salvo variantes híbridas)
Requisitos normativos de soberanía del dato Cumplibles Depende del proveedor y la región
Personal necesario Al menos 2 personas con conocimiento profundo y guardia Una persona a tiempo parcial
Tiempo hasta el primer clúster productivo Semanas Horas

Advertencia seria: operar un clúster propio en producción exige un equipo dedicado. No es una tarea que se pueda añadir a la lista de otro puesto. Necesitas cobertura para incidentes fuera de horario, un procedimiento probado de restauración de etcd, una política de parches del sistema operativo, y alguien que entienda los certificados cuando caduquen. Si tu organización no puede sostener eso, un clúster gestionado no es una comodidad: es una decisión de gestión de riesgo.

Cuándo kubeadm sí es la respuesta correcta: centro de datos propio o hardware específico (GPU, baja latencia, cumplimiento normativo); necesidad de configuraciones del plano de control que ningún gestionado permite; ahorro de coste con muchos nodos; y aprendizaje y certificación, porque el CKA (12-01) evalúa exactamente esto.

Para Rutas Norte montaremos un clúster con kubeadm en un laboratorio para aprender y practicar, con la conclusión razonada de producción reservada para 10-06.

  1. Preparación de las máquinas

Partimos de tres máquinas Ubuntu 24.04 LTS en el laboratorio de Rutas Norte:

Nombre Función vCPU RAM IP
rn-control-1 Plano de control 2 4 GB 10.10.0.11
rn-worker-1 Nodo de trabajo 2 4 GB 10.10.0.21
rn-worker-2 Nodo de trabajo 2 4 GB 10.10.0.22

Requisitos mínimos oficiales: 2 CPU y 2 GB de RAM por nodo de control, nombre de host único, dirección MAC única y product_uuid único (sudo cat /sys/class/dmi/id/product_uuid). Si dos máquinas clonadas comparten product_uuid, el clúster puede confundirlas: regenéralo desde el hipervisor.

3.1 Desactivar el swap

Todos los pasos siguientes se ejecutan en las tres máquinas.

sudo swapoff -a                                # ahora
sudo sed -i '/ swap / s/^/#/' /etc/fstab       # y de forma permanente
free -h                                        # comprobar que Swap está a 0

¿Por qué hay que desactivar el swap? Porque rompe el modelo de recursos que estudiamos en 03-04 y 03-05. El planificador decide dónde va un pod según la memoria solicitada y disponible; el kubelet expulsa pods cuando el nodo tiene presión de memoria, siguiendo las clases de QoS. Si hay swap, un pod que supera su límite de memoria no muere: empieza a paginar a disco, se vuelve mil veces más lento, sus sondas de liveness empiezan a fallar de forma errática y el nodo entero se degrada sin que ninguna métrica lo explique. Es un fallo mucho peor que un OOMKilled limpio.

Kubernetes 1.30 tiene soporte experimental de swap (NodeSwap), pero está en beta con muchas advertencias. Para un clúster de producción, swap desactivado.

3.2 Módulos del kernel

# Declarar los módulos que deben cargarse en cada arranque
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# Cargarlos ahora sin reiniciar
sudo modprobe overlay
sudo modprobe br_netfilter

# Verificar
lsmod | grep -E 'overlay|br_netfilter'

Qué hace cada uno:

  • overlay: el sistema de ficheros por capas que usa containerd para montar las imágenes de contenedor. Sin él, el runtime no puede crear contenedores.
  • br_netfilter: permite que el tráfico que atraviesa un puente de red de Linux (el que conecta los contenedores del nodo) sea visible para iptables. Sin él, kube-proxy escribe reglas que nunca se aplican al tráfico entre pods, y los Services simplemente no funcionan.

3.3 Parámetros sysctl

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system                                          # aplicar
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables # verificar
  • ip_forward = 1: el nodo debe reenviar paquetes entre interfaces. Sin esto, un pod de rn-worker-1 no puede hablar con uno de rn-worker-2.
  • bridge-nf-call-iptables = 1: activa lo que br_netfilter hace posible.

Si sysctl te dice que el parámetro no existe, es que br_netfilter no está cargado. El orden importa.

3.4 Cortafuegos y puertos

Nodo Puerto/rango Protocolo Componente Quién accede
Control 6443 TCP kube-apiserver Todos los nodos, kubectl, balanceador
Control 2379-2380 TCP etcd (cliente y par) Solo nodos de control
Control 10250 TCP kubelet API Plano de control (logs, exec)
Control 10257 TCP kube-controller-manager Local
Control 10259 TCP kube-scheduler Local
Trabajo 10250 TCP kubelet API Plano de control
Trabajo 10256 TCP kube-proxy (health) Balanceadores
Ambos 30000-32767 TCP Rango de NodePort Según necesidad
Ambos Según CNI UDP/TCP Red de pods Todos los nodos

Sobre el último: cada CNI usa lo suyo. Calico con VXLAN necesita UDP 4789; con IP-in-IP necesita el protocolo IP 4; Cilium con VXLAN, UDP 8472. Consulta la documentación del CNI que elijas.

Para el laboratorio basta con abrir esos puertos con ufw y permitir todo el tráfico de la red interna: sudo ufw allow from 10.10.0.0/24. Verifica desde otro nodo con nc -zv 10.10.0.11 6443.

Consejo de laboratorio: si estás aprendiendo y algo no conecta, desactiva temporalmente el cortafuegos (sudo ufw disable) para descartar que sea el culpable. En producción, jamás; ahí se documenta cada regla.

  1. Instalación de containerd

Kubernetes no ejecuta contenedores directamente: habla con un runtime a través de la interfaz CRI. Desde 1.24, Docker no es un runtime válido de forma directa; containerd es la opción estándar.

# 1. Repositorio de Docker (containerd se distribuye desde ahí)
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 2. Instalar SOLO containerd (no el motor de Docker)
sudo apt-get update && sudo apt-get install -y containerd.io

4.1 SystemdCgroup: el error más habitual

Aquí está el fallo número uno de los clústeres con kubeadm. containerd trae por defecto una configuración con SystemdCgroup = false, mientras que el kubelet de Kubernetes 1.30 usa el controlador de cgroups systemd. Si no coinciden, el clúster arranca aparentemente bien y luego los pods se reinician de forma aleatoria bajo presión de memoria, porque hay dos gestores de cgroups peleándose por el mismo nodo.

# 1. Generar la configuración por defecto (containerd viene sin config.toml completo)
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null

# 2. Cambiar SystemdCgroup de false a true
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# 3. VERIFICAR que el cambio se aplicó (no te fíes) y reiniciar
grep SystemdCgroup /etc/containerd/config.toml     # debe decir: SystemdCgroup = true
sudo systemctl restart containerd && sudo systemctl enable containerd

Si el grep no devuelve nada o devuelve false, la ruta de la opción ha cambiado en tu versión. Edita el fichero a mano y busca la sección [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options].

4.2 Comprobar que containerd responde a CRI

sudo apt-get install -y cri-tools
printf 'runtime-endpoint: unix:///run/containerd/containerd.sock\n' | sudo tee /etc/crictl.yaml
sudo crictl info | head -20

Si esto responde, el runtime está listo. crictl es tu herramienta de depuración a nivel de nodo: crictl ps, crictl images, crictl logs. Cuando el apiserver está caído, kubectl no sirve de nada y crictl es lo único que tienes.

  1. Instalación de kubeadm, kubelet y kubectl

Los tres se instalan desde el repositorio oficial de Kubernetes, que desde 1.28 está segmentado por versión menor. Esto es importante: el repositorio de v1.30 solo contiene parches de 1.30, lo que evita saltos accidentales de versión menor.

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

# Instalar una versión de parche CONCRETA, no la última
sudo apt-get install -y kubelet=1.30.4-1.1 kubeadm=1.30.4-1.1 kubectl=1.30.4-1.1

5.1 Fijar las versiones con apt-mark hold

sudo apt-mark hold kubelet kubeadm kubectl
apt-mark showhold                      # comprobar

# Habilitar el kubelet (aún fallará y reintentará: es normal, no hay
# configuración hasta que se ejecute kubeadm init o join)
sudo systemctl enable --now kubelet

Por qué el hold es imprescindible: sin él, un apt upgrade rutinario de mantenimiento del sistema actualizaría el kubelet en un nodo cualquiera. Ese kubelet nuevo podría ser incompatible con el plano de control, o simplemente reiniciarse en mitad del día y expulsar todos los pods de ese nodo. Las actualizaciones de Kubernetes son un procedimiento planificado (apartado 12), nunca un efecto secundario.

  1. kubeadm init con fichero de configuración

Podrías arrancar el clúster con una ristra de banderas (kubeadm init --pod-network-cidr=... --control-plane-endpoint=... --apiserver-cert-extra-sans=...), y funcionaría. Pero el problema es evidente después de todo lo que llevamos de curso: no está versionado, nadie recuerda qué banderas se usaron hace un año, y al añadir un nodo de control hay que repetirlas exactamente. La forma profesional es un fichero de configuración, guardado junto a los manifiestos.

# kubeadm/rn-cluster.yaml
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:                    # cómo anuncia ESTE nodo el apiserver
  advertiseAddress: "10.10.0.11"
  bindPort: 6443
nodeRegistration:
  name: "rn-control-1"
  criSocket: "unix:///run/containerd/containerd.sock"
  taints:                            # taint estándar del plano de control (06-05)
    - { key: "node-role.kubernetes.io/control-plane", effect: "NoSchedule" }
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "v1.30.4"
clusterName: "rutas-norte-lab"

# Punto de entrada ESTABLE. Se declara desde el primer momento aunque
# hoy solo haya un nodo: cambiarlo después obliga a regenerar certificados.
controlPlaneEndpoint: "api-k8s.rutasnorte.example:6443"

networking:
  # DEBE coincidir con lo que configures en Calico/Cilium, y NO puede
  # solaparse con la red física (10.10.0.0/24) ni con serviceSubnet.
  podSubnet: "10.244.0.0/16"
  serviceSubnet: "10.96.0.0/12"
  dnsDomain: "cluster.local"

apiServer:
  # Nombres e IP válidos del certificado del apiserver. Acceder por un
  # nombre no listado da "certificate is valid for ..., not ...".
  certSANs: ["api-k8s.rutasnorte.example", "10.10.0.10", "10.10.0.11",
             "localhost", "127.0.0.1"]
  extraArgs:                          # registro de auditoría (08-06)
    audit-log-path: "/var/log/kubernetes/audit.log"
    audit-log-maxage: "30"

etcd:
  local: { dataDir: "/var/lib/etcd" }  # etcd apilado, pod estático aquí
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: "systemd"               # DEBE coincidir con SystemdCgroup=true
# Recursos reservados al sistema y al kubelet, que NO se ofrecen al
# planificador. Sin esto, un nodo saturado se queda sin memoria para sshd.
systemReserved: { cpu: "200m", memory: "256Mi" }
kubeReserved:   { cpu: "200m", memory: "256Mi" }
evictionHard:   { memory.available: "200Mi", nodefs.available: "10%" }
serverTLSBootstrap: true

Ejecutamos:

# Primero, descargar las imágenes por adelantado (evita timeouts
# durante el init si la red es lenta)
sudo kubeadm config images pull --config kubeadm/rn-cluster.yaml

# Arrancar
sudo kubeadm init --config kubeadm/rn-cluster.yaml --upload-certs

--upload-certs sube los certificados del plano de control a un Secret temporal del clúster, cifrado, para que otros nodos de control puedan unirse sin copiarlos a mano. Caduca a las 2 horas.

Salida (muy abreviada):

[certs] Generating "ca" certificate and key
[certs] apiserver serving cert is signed for DNS names [api-k8s.rutasnorte.example
  kubernetes kubernetes.default rn-control-1] and IPs [10.96.0.1 10.10.0.11 10.10.0.10]
[control-plane] Creating static Pod manifest for "kube-apiserver"
[etcd] Creating static Pod manifest for local etcd
[apiclient] All control plane components are healthy after 12.503 seconds

Your Kubernetes control-plane has initialized successfully!
You should now deploy a pod network to the cluster.

You can now join any number of control-plane nodes:
  kubeadm join api-k8s.rutasnorte.example:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:1a2b3c... \
    --control-plane --certificate-key 8f3c1a...

Then you can join any number of worker nodes:
  kubeadm join api-k8s.rutasnorte.example:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:1a2b3c...

Guarda esa salida. Las dos órdenes join del final son las que necesitas en los apartados 9 y 10.

# Configurar kubectl para tu usuario
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

kubectl get nodes
NAME           STATUS     ROLES           AGE   VERSION
rn-control-1   NotReady   control-plane   62s   v1.30.4

NotReady. Es correcto, y lo explicamos en el apartado 8.

6.1 Campos que más importan

Campo Qué hace Qué pasa si lo pones mal
podSubnet Rango de IP de los pods Si se solapa con la red física o con serviceSubnet, el enrutado se rompe de formas muy difíciles de depurar
serviceSubnet Rango de las IP virtuales de los Services Igual; además la IP .1 de este rango es la del Service kubernetes
controlPlaneEndpoint Nombre estable del plano de control Sin él, no puedes añadir nodos de control después sin regenerar certificados
certSANs Nombres válidos del certificado del apiserver Acceder por un nombre no listado da error de certificado
kubernetesVersion Versión a instalar Si no coincide con los binarios instalados, init avisa
cgroupDriver Gestor de cgroups del kubelet Si no coincide con containerd, pods inestables bajo presión
criSocket Socket del runtime Con varios runtimes instalados, kubeadm no sabe cuál usar

Sobre controlPlaneEndpoint: aunque hoy solo tengas un nodo de control y no un balanceador, decláralo con un nombre DNS desde el principio. Puedes hacer que ese nombre apunte hoy a 10.10.0.11 y mañana a la IP virtual del balanceador. Si no lo haces, el certificado y todos los kubeconfig apuntarán a la IP del nodo, y montar HA más tarde exige regenerar certificados en todo el clúster.

  1. Qué crea exactamente kubeadm en el nodo

Este apartado conecta directamente con la arquitectura que vimos en 01-02. Ahora vamos a verla en el disco.

7.1 Pods estáticos del plano de control

ls -l /etc/kubernetes/manifests/
-rw------- 1 root root 2405 Sep 12 10:02 etcd.yaml
-rw------- 1 root root 3891 Sep 12 10:02 kube-apiserver.yaml
-rw------- 1 root root 3320 Sep 12 10:02 kube-controller-manager.yaml
-rw------- 1 root root 1463 Sep 12 10:02 kube-scheduler.yaml

Esto es un pod estático: el kubelet vigila ese directorio y arranca cualquier pod que encuentre en él, sin pasar por el apiserver. Es la solución al problema del huevo y la gallina: ¿cómo arranca el apiserver, si para crear un pod hace falta un apiserver? Respuesta: no se crea como un pod normal, lo lee el kubelet directamente del disco.

Consecuencias prácticas muy importantes:

  • Si editas /etc/kubernetes/manifests/kube-apiserver.yaml, el kubelet detecta el cambio y reinicia el apiserver automáticamente en segundos. Es la forma de añadir una bandera a mano.
  • Si borras ese fichero, el apiserver desaparece y pierdes el clúster. Copia de seguridad antes de tocar nada.
  • kubectl delete pod kube-apiserver-rn-control-1 -n kube-system no lo elimina de verdad: el kubelet lo recrea, porque la fuente de verdad es el fichero.
  • Si el apiserver no arranca, kubectl no funciona. Depúralo con crictl ps -a y crictl logs, o con journalctl -u kubelet.
# Los pods estáticos aparecen como objetos "espejo" en la API,
# con el nombre del nodo como sufijo: esa es su marca distintiva.
kubectl get pods -n kube-system -l tier=control-plane
etcd-rn-control-1                      1/1   Running   0   3m
kube-apiserver-rn-control-1            1/1   Running   0   3m
kube-controller-manager-rn-control-1   1/1   Running   0   3m
kube-scheduler-rn-control-1            1/1   Running   0   3m

7.2 Certificados

sudo ls -1 /etc/kubernetes/pki/
apiserver.crt  apiserver.key  apiserver-etcd-client.crt  apiserver-etcd-client.key
apiserver-kubelet-client.crt  apiserver-kubelet-client.key  ca.crt  ca.key  etcd/
front-proxy-ca.crt  front-proxy-ca.key  front-proxy-client.crt  front-proxy-client.key
sa.key  sa.pub
Fichero Función
ca.crt / ca.key Autoridad certificadora raíz del clúster. Es el secreto más valioso: quien la tiene puede emitir credenciales de administrador
apiserver.crt Certificado de servidor del apiserver, con los certSANs
apiserver-kubelet-client.* Con qué se identifica el apiserver al hablar con los kubelets (kubectl logs, exec)
apiserver-etcd-client.* Con qué se identifica el apiserver ante etcd
etcd/ CA y certificados propios de etcd (servidor, par, sanidad)
front-proxy-* Para el agregador de API (métricas personalizadas de 09-01)
sa.key / sa.pub Par de claves con el que se firman los tokens de las ServiceAccounts (03-06)

Para inspeccionar los nombres válidos de un certificado: sudo openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text | grep -A2 'Subject Alternative Name'.

7.3 Ficheros kubeconfig

En /etc/kubernetes/*.conf hay uno por componente: controller-manager.conf, scheduler.conf, kubelet.conf (credenciales de este nodo, con rotación automática) y dos de administración:

  • admin.conf: el que copiaste a ~/.kube/config. Desde 1.29 pertenece al grupo kubeadm:cluster-admins, sujeto a RBAC (08-01).
  • super-admin.conf: salta el RBAC por completo (system:masters). Es el rompecristales cuando has roto el RBAC y no puedes ni arreglarlo. No lo repartas.

7.4 Datos de etcd

En /var/lib/etcd/member/ (con sus directorios snap y wal) vive el estado completo de tu clúster: todos los objetos, todos los Secrets, todo. Es lo que hay que respaldar (apartado 13).

  1. Instalar el CNI y por qué los nodos están NotReady

kubectl get nodes
kubectl describe node rn-control-1 | grep -A5 Conditions
  Type             Status  Reason                       Message
  Ready            False   KubeletNotReady              container runtime network not
                                                        ready: NetworkReady=false
                                                        reason:NetworkPluginNotReady
                                                        message:Network plugin returns
                                                        error: cni plugin not initialized

El mensaje es explícito. El kubelet se niega a declararse Ready mientras no exista un complemento de red que sepa asignar IP a los pods. Recuerda de 04-01: Kubernetes define el contrato (todo pod tiene una IP, todos los pods se ven sin NAT) pero no lo implementa. Eso es trabajo del CNI.

Verás también CoreDNS en Pending, y es normal: CoreDNS es un pod y necesita red para arrancar.

Instalamos Calico (el detalle de los CNI está en 04-01; aquí solo lo necesario):

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/tigera-operator.yaml

cat <<EOF | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata: { name: default }
spec:
  calicoNetwork:
    ipPools:
      # DEBE coincidir con networking.podSubnet del fichero de kubeadm
      - { cidr: 10.244.0.0/16, encapsulation: VXLANCrossSubnet, natOutgoing: Enabled }
EOF

kubectl get nodes -w      # NotReady -> Ready en menos de un minuto

Y CoreDNS arranca solo. La secuencia completa es:

sequenceDiagram
    participant K as kubeadm init
    participant Kl as kubelet
    participant A as apiserver
    participant C as CNI (Calico)
    K->>Kl: escribe pods estáticos + config
    Kl->>A: arranca apiserver, etcd, scheduler, cm
    Kl->>A: registra el nodo (NotReady: sin red)
    Note over A: CoreDNS queda Pending
    K-->>C: (tú aplicas los manifiestos del CNI)
    C->>Kl: instala el binario CNI en /opt/cni/bin
    Kl->>A: NetworkReady=true → nodo Ready
    A->>A: CoreDNS se planifica y arranca

  1. Unir nodos de trabajo

En rn-worker-1 y rn-worker-2, ya preparados con los apartados 3, 4 y 5:

sudo kubeadm join api-k8s.rutasnorte.example:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:1a2b3c4d5e6f...
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster.

Qué significan los dos parámetros:

  • --token: una credencial temporal (caduca a las 24 horas) que autoriza al nodo a pedir su certificado. Es un Secret de tipo bootstrap.kubernetes.io/token en kube-system.
  • --discovery-token-ca-cert-hash: el hash SHA-256 de la clave pública de la CA del clúster. Sirve para que el nodo verifique al apiserver, no al revés. Sin él, un atacante podría suplantar el plano de control y capturar el token. Nunca uses --discovery-token-unsafe-skip-ca-verification fuera de un laboratorio.

9.1 Generar un token nuevo cuando caduca

Es la situación más habitual: quieres añadir un nodo tres semanas después y el token original ya no vale.

# En un nodo de control: una sola orden que imprime todo lo necesario
sudo kubeadm token create --print-join-command
kubeadm join api-k8s.rutasnorte.example:6443 --token 7t8u9i.qwertyuiopasdfgh \
  --discovery-token-ca-cert-hash sha256:1a2b3c4d5e6f...

Órdenes relacionadas:

sudo kubeadm token list                            # tokens vivos y caducidad
sudo kubeadm token create --ttl 2h --print-join-command
sudo kubeadm token delete 7t8u9i.qwertyuiopasdfgh

# Recalcular el hash de la CA a mano, si perdiste la salida del init
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
  openssl rsa -pubin -outform der 2>/dev/null | \
  openssl dgst -sha256 -hex | sed 's/^.* //'

9.2 Etiquetar los nodos

kubeadm no pone la etiqueta de rol en los nodos de trabajo por seguridad (un kubelet no puede autoasignarse etiquetas de rol). Se hace desde el plano de control:

kubectl label node rn-worker-1 node-role.kubernetes.io/worker=
kubectl label node rn-worker-2 node-role.kubernetes.io/worker=
# Y las etiquetas de topología de Rutas Norte (09-05)
kubectl label node rn-worker-1 topology.kubernetes.io/zone=lab-a
kubectl label node rn-worker-2 topology.kubernetes.io/zone=lab-b
kubectl get nodes                 # los tres Ready, con sus roles

  1. Plano de control en alta disponibilidad

Con un solo nodo de control, si rn-control-1 cae, el clúster sigue sirviendo tráfico (los pods siguen corriendo, kube-proxy sigue enrutando) pero pierdes toda capacidad de gestión: no hay reprogramación de pods, ni escalado, ni despliegues, ni HPA. Y si el disco de etcd se pierde, pierdes el clúster entero.

10.1 El punto de entrada estable

Todos los nodos de control publican el mismo puerto 6443. Necesitas algo delante que reparta:

flowchart TB
    K[kubectl / kubelets<br/>de los nodos] --> LB["api-k8s.rutasnorte.example:6443<br/>(IP virtual 10.10.0.10)"]
    LB --> C1[rn-control-1<br/>:6443]
    LB --> C2[rn-control-2<br/>:6443]
    LB --> C3[rn-control-3<br/>:6443]
    C1 -.-> E[(etcd apilado<br/>quórum de 3)]
    C2 -.-> E
    C3 -.-> E

Opciones: HAProxy + keepalived con una IP virtual flotante (lo más común en centro de datos propio), el balanceador de capa 4 de la nube si estás en máquinas virtuales, o kube-vip como pod estático si no quieres infraestructura externa. Lo que no vale es un DNS con varios registros A: el cliente cachea y no detecta caídas.

Configuración mínima de HAProxy:

# /etc/haproxy/haproxy.cfg
frontend k8s-api
    bind *:6443
    mode tcp
    default_backend k8s-control-plane
backend k8s-control-plane
    mode tcp
    option tcp-check
    balance roundrobin
    server rn-control-1 10.10.0.11:6443 check fall 3 rise 2
    server rn-control-2 10.10.0.12:6443 check fall 3 rise 2
    server rn-control-3 10.10.0.13:6443 check fall 3 rise 2

Es TCP puro (modo tcp), no HTTP: la conexión al apiserver es TLS de extremo a extremo y el balanceador no debe terminarla. Con check, si un nodo deja de responder en 6443, HAProxy deja de enviarle tráfico automáticamente.

10.2 etcd apilado frente a etcd externo

Aspecto etcd apilado etcd externo
Dónde corre Como pod estático en cada nodo de control En máquinas dedicadas
Máquinas necesarias 3 (control + etcd juntos) 3 de control + 3 de etcd = 6
Configuración Automática con kubeadm Manual: certificados, unidades systemd
Aislamiento de fallos Perder un nodo pierde un control y un miembro de etcd Independientes
Rendimiento bajo carga etcd compite con el apiserver por E/S y CPU etcd tiene su disco para él
Recomendación Por defecto, para la mayoría Clústeres muy grandes o con requisitos estrictos

etcd necesita quórum: 1 miembro tolera 0 fallos, 2 miembros también toleran 0 (¡peor que 1!), 3 toleran 1, 4 toleran 1, y 5 toleran 2. Por eso el número de nodos de control es siempre impar: 1 (laboratorio), 3 (producción normal) o 5 (clústeres grandes).

Para etcd externo, se declara en el fichero de kubeadm:

apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
etcd:
  external:
    endpoints:
      - https://10.10.0.31:2379
      - https://10.10.0.32:2379
      - https://10.10.0.33:2379
    caFile: /etc/kubernetes/pki/etcd/ca.crt
    certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
    keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key

10.3 Unir nodos de control adicionales

# En rn-control-2 y rn-control-3 (ya preparados con 3, 4 y 5)
sudo kubeadm join api-k8s.rutasnorte.example:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:1a2b3c... \
  --control-plane \
  --certificate-key 8f3c1a...

La --certificate-key viene de --upload-certs. Si han pasado más de 2 horas, se regenera:

# En rn-control-1
sudo kubeadm init phase upload-certs --upload-certs
[upload-certs] Using certificate key:
f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d

Para verificar la salud del clúster de etcd, kubectl -n kube-system exec etcd-rn-control-1 -- etcdctl ... endpoint status --cluster -w table debe mostrar los tres miembros, con la misma versión, tamaño de base de datos parecido y exactamente uno con IS LEADER = true.

  1. Gestión de certificados

Aquí está la trampa que sorprende a más equipos: los certificados que genera kubeadm caducan al año. Un clúster montado en marzo deja de funcionar en marzo del año siguiente, sin previo aviso, con un mensaje así:

Unable to connect to the server: x509: certificate has expired or is not yet valid

Kubernetes renueva automáticamente los certificados durante kubeadm upgrade. Como muchos equipos actualizan al menos una vez al año, nunca lo ven. El que no actualiza, se lleva el susto.

11.1 Comprobar la caducidad

sudo kubeadm certs check-expiration
CERTIFICATE                 EXPIRES                  RESIDUAL TIME
admin.conf                 Sep 12, 2026 10:02 UTC    364d
apiserver                  Sep 12, 2026 10:02 UTC    364d
apiserver-etcd-client      Sep 12, 2026 10:02 UTC    364d
apiserver-kubelet-client   Sep 12, 2026 10:02 UTC    364d
etcd-server, etcd-peer, scheduler.conf, controller-manager.conf ... 364d

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME
ca, etcd-ca, front-proxy-ca   Sep 10, 2035 10:02 UTC   9y

Las CA duran 10 años; los certificados que firman, 1 año.

Recomendación de operación: no lo dejes al calendario humano. El apiserver expone apiserver_client_certificate_expiration_seconds_bucket, sobre la que se puede montar una alerta en Alertmanager (07-04). Más simple y más fiable todavía: un CronJob (06-03) o una tarea del sistema que ejecute kubeadm certs check-expiration semanalmente y avise si queda menos de un mes.

11.2 Renovar

# EN CADA NODO DE CONTROL
sudo cp -r /etc/kubernetes/pki /root/pki-respaldo-$(date +%F)   # copia primero
sudo kubeadm certs renew all              # o solo uno: kubeadm certs renew apiserver

# Reiniciar los pods estáticos: basta con sacarlos y devolverlos
sudo mkdir -p /tmp/manifests-parados
sudo mv /etc/kubernetes/manifests/*.yaml /tmp/manifests-parados/
sleep 20
sudo mv /tmp/manifests-parados/*.yaml /etc/kubernetes/manifests/

# Actualizar tu kubeconfig personal, que también se renovó
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

sudo kubeadm certs check-expiration && kubectl get nodes

Nota sobre el kubelet: su certificado se rota solo, automáticamente, si rotateCertificates: true (por defecto). Los certificados que hay que renovar a mano son los del plano de control.

  1. Actualización de versión

12.1 La regla del salto de una versión menor

Solo puedes saltar una versión menor cada vez. De 1.28 a 1.30 no se puede ir directo: hay que hacer 1.28 → 1.29 → 1.30. Cada salto es un procedimiento completo.

Además, la política de desfase (version skew) exige:

Componente Desfase permitido respecto al apiserver
kube-controller-manager, kube-scheduler Hasta 1 menor por debajo
kubelet Hasta 3 menores por debajo
kube-proxy Hasta 3 menores por debajo
kubectl 1 por encima o 1 por debajo

De ahí el orden obligatorio: primero el plano de control, después los nodos de trabajo. Nunca al revés.

12.2 Antes de empezar

Tres cosas, por este orden: copia de seguridad de etcd (apartado 13, no negociable), leer las notas de la versión, y comprobar que nada usa APIs que desaparecen.

kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
apiserver_requested_deprecated_apis{group="flowcontrol.apiserver.k8s.io",
  removed_release="1.32",resource="flowschemas",version="v1beta3"} 1

Cualquier línea aquí es trabajo que hay que hacer antes de actualizar. Herramientas como kubent (kube-no-trouble) o pluto escanean tus manifiestos buscando lo mismo.

12.3 Primer nodo de control

# --- EN rn-control-1 ---

# 1. Liberar el hold y actualizar SOLO kubeadm
sudo apt-mark unhold kubeadm
sudo apt-get update

# Cambiar el repositorio a la nueva versión menor
sudo sed -i 's|v1.30|v1.31|' /etc/apt/sources.list.d/kubernetes.list
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
  sudo gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
sudo apt-get update

sudo apt-get install -y kubeadm=1.31.1-1.1
sudo apt-mark hold kubeadm
kubeadm version
# 2. Ver el plan: qué se va a actualizar y a qué
sudo kubeadm upgrade plan
COMPONENT                 CURRENT   TARGET
kube-apiserver            v1.30.4   v1.31.1
kube-controller-manager   v1.30.4   v1.31.1
kube-scheduler            v1.30.4   v1.31.1
kube-proxy                v1.30.4   v1.31.1
CoreDNS                   v1.11.1   v1.11.3
etcd                      3.5.14    3.5.15

Components that must be upgraded manually: kubelet en los 3 nodos.
You can now apply the upgrade by executing:  kubeadm upgrade apply v1.31.1
# 3. Aplicar. Actualiza los manifiestos de los pods estáticos y
#    RENUEVA LOS CERTIFICADOS de paso.
sudo kubeadm upgrade apply v1.31.1
[upgrade/successful] SUCCESS! Your cluster was upgraded to "v1.31.1".
# 4. Drenar el nodo: mover sus pods a otro sitio y no aceptar nuevos.
#    Respeta los PodDisruptionBudgets de 09-05.
kubectl drain rn-control-1 --ignore-daemonsets --delete-emptydir-data

# 5. Actualizar kubelet y kubectl
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet=1.31.1-1.1 kubectl=1.31.1-1.1
sudo apt-mark hold kubelet kubectl

sudo systemctl daemon-reload
sudo systemctl restart kubelet

# 6. Devolver el nodo al servicio
kubectl uncordon rn-control-1
kubectl get nodes
NAME           STATUS   ROLES           AGE   VERSION
rn-control-1   Ready    control-plane   3d    v1.31.1
rn-worker-1    Ready    worker          3d    v1.30.4
rn-worker-2    Ready    worker          3d    v1.30.4

12.4 Resto de nodos, uno a uno

Para los demás nodos de control y para los de trabajo, el procedimiento es el mismo del apartado anterior con una sola diferencia: se usa sudo kubeadm upgrade node en vez de upgrade apply.

# En cada nodo restante, de uno en uno:
sudo apt-mark unhold kubeadm && sudo apt-get install -y kubeadm=1.31.1-1.1 && sudo apt-mark hold kubeadm
sudo kubeadm upgrade node                                    # <-- la diferencia
kubectl drain <nodo> --ignore-daemonsets --delete-emptydir-data --timeout=300s
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet=1.31.1-1.1 kubectl=1.31.1-1.1
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload && sudo systemctl restart kubelet
kubectl uncordon <nodo>
kubectl get nodes    # confirmar Ready y v1.31.1 ANTES de pasar al siguiente

Uno a uno, verificando entre medias. Si drain se bloquea, es un PodDisruptionBudget haciendo su trabajo: postgres-reservas no puede quedarse sin réplicas. Investiga con kubectl get pdb -A antes de forzar nada.

  1. Copia de seguridad y restauración de etcd

En 05-06 hicimos copias de los datos de las aplicaciones con Velero. Esto es otra cosa: es la copia de la definición del clúster. Todos los Deployments, Secrets, ConfigMaps, RBAC, CRDs... todo vive en etcd.

Sin copia de etcd, un fallo del disco del nodo de control te obliga a recrear el clúster desde cero y volver a aplicar todos los manifiestos. Con GitOps (10-05) eso sería recuperable, pero perderías todo lo que no esté en Git.

13.1 Hacer la copia

sudo apt-get install -y etcd-client

# En un nodo de control
sudo ETCDCTL_API=3 etcdctl snapshot save /var/respaldos/etcd-$(date +%F-%H%M).db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

# Verificar (¡una copia no verificada no es una copia!)
sudo etcdutl --write-out=table snapshot status /var/respaldos/etcd-2026-09-12-1042.db
+----------+----------+------------+------------+
|   HASH   | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 4f2a91bc |   184920 |       1847 |      28 MB |
+----------+----------+------------+------------+

Esa misma orden en /etc/cron.d/respaldo-etcd, a las 03:15 diarias y encadenada con un find /var/respaldos -name 'etcd-*.db' -mtime +14 -delete, cubre la automatización con retención de dos semanas.

Tres cosas imprescindibles: copia también /etc/kubernetes/pki/ (el snapshot sin la CA es inútil), sácalas del nodo (una copia en el disco que se va a estropear no es una copia) y cífralas, porque contienen todos los Secrets de rutas-norte-pro, incluidas las credenciales de postgres-reservas y las de la pasarela de pagos: recuerda de 03-02 que los Secrets están en base64, no cifrados.

13.2 Restaurar

Procedimiento de emergencia, para cuando el plano de control es irrecuperable:

# 1. Parar el plano de control sacando los pods estáticos
sudo mkdir -p /tmp/manifests-parados
sudo mv /etc/kubernetes/manifests/*.yaml /tmp/manifests-parados/
sudo crictl ps                      # esperar a que no quede nada

# 2. Apartar los datos actuales (NO borrarlos: por si acaso)
sudo mv /var/lib/etcd /var/lib/etcd-roto-$(date +%F)

# 3. Restaurar el snapshot en un directorio nuevo
sudo etcdutl snapshot restore /var/respaldos/etcd-2026-09-12-1042.db \
  --data-dir=/var/lib/etcd --name=rn-control-1 \
  --initial-cluster=rn-control-1=https://10.10.0.11:2380 \
  --initial-advertise-peer-urls=https://10.10.0.11:2380

# 4. Devolver los pods estáticos y verificar
sudo mv /tmp/manifests-parados/*.yaml /etc/kubernetes/manifests/
sleep 60 && kubectl get nodes && kubectl get pods -A

Puntos críticos:

  • En un clúster HA, hay que restaurar en los tres nodos de control, con el snapshot idéntico y cada uno con su --name y sus URL. Si los datos difieren, etcd no formará quórum.
  • El estado vuelve al momento de la copia. Todo lo creado después se pierde.
  • Los pods que estaban corriendo siguen corriendo (el kubelet no se ha enterado), pero pueden aparecer objetos "fantasma": pods que existen en el nodo pero no en etcd, o al revés. Después de restaurar, revisa kubectl get pods -A contra crictl ps en cada nodo.

Ensaya la restauración. Una copia de seguridad que nunca se ha restaurado no es una copia de seguridad: es un fichero. Hazlo en el laboratorio, cronométrate, y escribe el procedimiento en un runbook (11-06).

  1. kubeadm reset y limpieza

Cuando algo sale mal y quieres volver a empezar, o retirar un nodo:

# 1. Desde el plano de control: drenar y quitar el nodo del clúster
kubectl drain rn-worker-2 --ignore-daemonsets --delete-emptydir-data
kubectl delete node rn-worker-2

# 2. En el propio nodo: deshacer lo que hizo kubeadm
sudo kubeadm reset -f
[reset] Deleting contents of directories: [/etc/kubernetes/manifests
  /var/lib/kubelet /etc/kubernetes/pki]
The reset process does not clean CNI configuration. To do so, you must
remove /etc/cni/net.d
The reset process does not reset or clean up iptables rules.

reset es honesto sobre lo que no limpia. Hay que rematarlo a mano:

sudo rm -rf /etc/cni/net.d /opt/cni/bin/calico*        # configuración del CNI
sudo iptables -F && sudo iptables -t nat -F            # reglas de kube-proxy
sudo iptables -t mangle -F && sudo iptables -X
sudo ipvsadm -C 2>/dev/null || true                    # si usaba modo IPVS
for i in cni0 flannel.1 vxlan.calico; do               # interfaces virtuales
  sudo ip link delete "$i" 2>/dev/null || true
done
rm -rf $HOME/.kube

Si te saltas los pasos 3, 4 y 5, el siguiente kubeadm init o join en esa máquina puede parecer que funciona y luego dar fallos de red inexplicables: reglas viejas de iptables enrutando a IP de pods que ya no existen. Es una de las causas más frecuentes de «reinstalé y sigue fallando».

Errores Comunes y Consejos

1. SystemdCgroup = false en containerd. Es el error número uno. El clúster arranca, todo parece bien, y días después los pods se reinician bajo presión de memoria sin explicación. Verifica siempre con grep SystemdCgroup /etc/containerd/config.toml y reinicia containerd después de cambiarlo.

2. Olvidar swapoff -a en /etc/fstab. Lo desactivas, funciona, reinicias la máquina un mes después y el kubelet no arranca. Comenta la línea en /etc/fstab siempre.

3. podSubnet que se solapa con la red física. Si tu laboratorio usa 10.10.0.0/24 y declaras podSubnet: 10.0.0.0/8, el enrutado se rompe de formas que parecen magia negra. Elige rangos que no colisionen con nada: ni con la red de nodos, ni con serviceSubnet, ni con la red corporativa.

4. El rango del CNI distinto de podSubnet. Declaras 10.244.0.0/16 en kubeadm e instalas Calico con su valor por defecto 192.168.0.0/16. Los pods obtienen IP del rango del CNI, pero el controller-manager asigna bloques del otro. Deben coincidir.

5. No declarar controlPlaneEndpoint desde el principio. Añadir alta disponibilidad después obliga a regenerar certificados en todo el clúster. Declara un nombre DNS desde el primer día, aunque apunte a una sola IP.

6. Certificados caducados al año. Real y muy común. Alerta en el calendario, o mejor, en la monitorización.

7. Saltar dos versiones menores, o actualizar los nodos antes que el plano de control. kubeadm upgrade apply v1.32.0 desde 1.30 es rechazado; y un kubelet 1.31 contra un apiserver 1.30 está fuera de la política de desfase. Una menor cada vez, plano de control primero.

8. kubectl drain bloqueado y forzarlo con --force. Lo que te bloquea suele ser un PDB (09-05) protegiendo postgres-reservas o redis-cache. Forzarlo puede provocar pérdida de datos. Investiga antes con kubectl get pdb -A.

9. Copias de etcd sin /etc/kubernetes/pki. El snapshot sin la CA no te devuelve un clúster funcional. Copia ambos, juntos, cifrados y fuera del nodo.

10. kubeadm reset sin limpiar iptables y CNI. Restos que provocan fallos de red en la reinstalación. Ejecuta la limpieza completa del apartado 14.

11. No fijar apt-mark hold. Un apt upgrade rutinario que actualice el kubelet en producción es un incidente esperando a ocurrir.

Ejercicios

Ejercicio 1: script de preparación de nodo

Escribe un script preparar-nodo.sh idempotente (que se pueda ejecutar dos veces sin romper nada) que deje una máquina Ubuntu lista para kubeadm join: swap desactivado de forma permanente, módulos y sysctl, containerd con SystemdCgroup=true verificado, y kubeadm/kubelet/kubectl 1.30.4 fijados. El script debe fallar con un mensaje claro si alguna verificación no pasa.

Ejercicio 2: fichero de configuración para HA

Escribe el fichero de configuración de kubeadm para el clúster rutas-norte-lab con estos requisitos: tres nodos de control detrás de api-k8s.rutasnorte.example (IP virtual 10.10.0.10), podSubnet 172.16.0.0/16, etcd apilado, auditoría activada, y el kubelet reservando 500m de CPU y 512Mi de memoria para el sistema. Explica por qué elegiste 172.16.0.0/16 y qué comprobarías antes.

Ejercicio 3: plan de actualización y recuperación

Redacta el procedimiento completo, en orden y con las órdenes exactas, para actualizar el clúster de 1.30.4 a 1.31.1 con un nodo de control y dos de trabajo, incluyendo la copia de seguridad previa. Añade el criterio de "punto sin retorno" y qué harías si el apiserver no arranca tras upgrade apply.

Soluciones

Solución 1

#!/usr/bin/env bash
# preparar-nodo.sh — idempotente
set -euo pipefail
VER="1.30.4-1.1"

sudo swapoff -a
sudo sed -i '/\sswap\s/ s/^\([^#]\)/#\1/' /etc/fstab     # no recomenta lo ya comentado
[[ $(swapon --show | wc -l) -eq 0 ]] || { echo "ERROR: swap activo"; exit 1; }

printf 'overlay\nbr_netfilter\n' | sudo tee /etc/modules-load.d/k8s.conf >/dev/null
sudo modprobe overlay; sudo modprobe br_netfilter
printf 'net.bridge.bridge-nf-call-iptables=1\nnet.bridge.bridge-nf-call-ip6tables=1\nnet.ipv4.ip_forward=1\n' \
  | sudo tee /etc/sysctl.d/k8s.conf >/dev/null
sudo sysctl --system >/dev/null
[[ $(sysctl -n net.ipv4.ip_forward) == 1 ]] || { echo "ERROR: ip_forward"; exit 1; }

command -v containerd >/dev/null || sudo apt-get install -y containerd.io
sudo mkdir -p /etc/containerd
[[ -s /etc/containerd/config.toml ]] || \
  containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep -q 'SystemdCgroup = true' /etc/containerd/config.toml || { echo "ERROR: cgroup"; exit 1; }
sudo systemctl restart containerd && sudo systemctl enable containerd

sudo apt-mark unhold kubelet kubeadm kubectl 2>/dev/null || true
sudo apt-get install -y kubelet="$VER" kubeadm="$VER" kubectl="$VER"
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubelet

Claves de idempotencia: el sed que no recomenta líneas ya comentadas, el [[ -s ]] antes de regenerar la configuración de containerd, y el unhold antes de instalar.

Solución 2

apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "v1.30.4"
clusterName: "rutas-norte-lab"
controlPlaneEndpoint: "api-k8s.rutasnorte.example:6443"
networking:
  podSubnet: "172.16.0.0/16"
  serviceSubnet: "10.96.0.0/12"
apiServer:
  certSANs: ["api-k8s.rutasnorte.example", "10.10.0.10",
             "10.10.0.11", "10.10.0.12", "10.10.0.13"]
  extraArgs:
    audit-log-path: "/var/log/kubernetes/audit.log"
    audit-policy-file: "/etc/kubernetes/audit-policy.yaml"
    audit-log-maxage: "30"
  extraVolumes:                       # montar la política en el pod estático
    - { name: audit-policy, hostPath: /etc/kubernetes/audit-policy.yaml,
        mountPath: /etc/kubernetes/audit-policy.yaml, readOnly: true, pathType: File }
etcd:
  local: { dataDir: "/var/lib/etcd" }
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: "systemd"
systemReserved: { cpu: "500m", memory: "512Mi" }
kubeReserved:   { cpu: "200m", memory: "256Mi" }
evictionHard:   { memory.available: "200Mi", nodefs.available: "10%" }

Por qué 172.16.0.0/16: es rango privado (RFC 1918) que no colisiona con la red del laboratorio (10.10.0.0/24) ni con serviceSubnet (10.96.0.0/12), y da 65 536 IP, suficientes. Comprobaciones previas: que 172.16/16 no se use en la red corporativa ni en la VPN, y configurar el CNI con ese mismo CIDR.

Solución 3

# --- FASE 0: copia previa (punto de retorno) ---
sudo etcdctl snapshot save /var/respaldos/pre-131.db --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key
sudo tar czf /var/respaldos/pki-pre-131.tgz /etc/kubernetes/pki
sudo etcdutl --write-out=table snapshot status /var/respaldos/pre-131.db  # verificar
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis     # APIs obsoletas

# --- FASE 1: rn-control-1 ---
sudo apt-mark unhold kubeadm
sudo sed -i 's|v1.30|v1.31|' /etc/apt/sources.list.d/kubernetes.list
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
  sudo gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
sudo apt-get update && sudo apt-get install -y kubeadm=1.31.1-1.1 && sudo apt-mark hold kubeadm
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.31.1        # <-- PUNTO SIN RETORNO
kubectl drain rn-control-1 --ignore-daemonsets --delete-emptydir-data
sudo apt-mark unhold kubelet kubectl && sudo apt-get install -y kubelet=1.31.1-1.1 kubectl=1.31.1-1.1
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload && sudo systemctl restart kubelet
kubectl uncordon rn-control-1 && kubectl get nodes

# --- FASE 2 y 3: rn-worker-1, luego rn-worker-2 (uno a uno) ---
# repositorio + kubeadm + `sudo kubeadm upgrade node` + drain + kubelet + uncordon

Punto sin retorno: kubeadm upgrade apply, porque migra el esquema de etcd. Antes, basta con revertir los paquetes; después, la vuelta atrás exige restaurar el snapshot.

Si el apiserver no arranca: sudo crictl ps -a | grep apiserver, sudo crictl logs <id> y journalctl -u kubelet -f. Causas típicas: una bandera nueva no soportada o un certificado mal generado. Si no se resuelve en la ventana de mantenimiento, restaurar pre-131.db y pki-pre-131.tgz con el procedimiento del apartado 13.2 y revertir los paquetes a 1.30.4.

Conclusión

Ya sabes montar un clúster de Kubernetes desde cero sobre máquinas propias. Lo esencial:

  • kubeadm arranca un clúster conforme; no aprovisiona máquinas ni instala el CNI. Esa frontera explica el 90 % de las dudas iniciales, incluido el nodo NotReady.
  • La preparación de las máquinas —swap, overlay y br_netfilter, ip_forward, puertos, y sobre todo SystemdCgroup = true en containerd— es donde nacen los fallos más difíciles de diagnosticar.
  • Un fichero de configuración versionado (ClusterConfiguration, InitConfiguration, KubeletConfiguration) es infinitamente mejor que una ristra de banderas; controlPlaneEndpoint y certSANs bien puestos desde el primer día te ahorran regenerar certificados más tarde.
  • kubeadm deja en el nodo pods estáticos en /etc/kubernetes/manifests, los certificados en /etc/kubernetes/pki y los kubeconfig: la arquitectura de 01-02, hecha ficheros.
  • Los nodos se unen con token y hash de la CA; el token caduca a las 24 horas y se regenera con kubeadm token create --print-join-command. La alta disponibilidad exige un punto de entrada estable y un número impar de miembros de etcd, normalmente 3 apilados.
  • Los certificados caducan al año, las actualizaciones van una versión menor cada vez con el plano de control antes que los nodos de trabajo, y las copias de etcd (snapshot + pki, cifradas, fuera del nodo, con restauración ensayada) son la diferencia entre un mal día y el fin de la empresa.

Y la advertencia que hay que repetir: operar esto en producción exige un equipo dedicado con guardia. En 10-06 veremos qué te ahorra un clúster gestionado y a qué precio.

Pero antes tenemos que resolver el problema con el que empezó este módulo. Ya sabemos crear clústeres, locales y propios; lo que sigue sin resolverse son los 120 ficheros YAML duplicados de Rutas Norte. En la siguiente lección, Helm, conoceremos el gestor de paquetes de Kubernetes: charts, valores, plantillas y releases. Es la herramienta con la que ya instalamos cert-manager (04-05) y kube-prometheus-stack (07-03) sin explicarla; toca entenderla de verdad y usarla para empaquetar la plataforma Rutas Norte en un único chart con un fichero de valores por entorno.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados