Apr 25, 2026

Kubernetes The Hard Way en espanol: entender el cluster pieza por pieza

Una guia larga y practica para estudiar Kubernetes desde PKI, etcd, control plane, workers, CNI, DNS, backups y troubleshooting, siguiendo el espiritu de Kubernetes The Hard Way.

Kubernetes The Hard Way en espanol

Kubernetes se entiende mejor cuando deja de ser una caja negra. kind, kubeadm, EKS, AKS y GKE son herramientas excelentes para operar rapido, pero tambien esconden decisiones importantes: certificados, kubeconfigs, etcd, control plane, kubelet, kube-proxy, runtime, CNI, DNS, autorizacion y smoke tests.

Kubernetes The Hard Way va en la direccion contraria. La idea no es tener un cluster listo lo antes posible. La idea es construirlo a mano para entender que hace cada pieza, que identidad necesita, como fluye el trafico y por que el orden de instalacion importa.

El repositorio original es de Kelsey Hightower: kelseyhightower/kubernetes-the-hard-way. Su valor no esta en reemplazar herramientas modernas, sino en formar criterio. Despues de hacerlo a mano, kubeadm, Helm, Terraform, Talos, Argo CD o un cluster gestionado dejan de sentirse magicos: empiezas a reconocer que problema resuelve cada automatizacion.

Que significa "the hard way"

"Hard way" no significa hacerlo dificil por orgullo. Significa quitar capas de abstraccion hasta ver la arquitectura real:

  • Una PKI que define identidades.
  • Un API server que autentica, autoriza, admite y persiste.
  • Un etcd que guarda el estado del cluster.
  • Un scheduler que asigna Pods a nodos.
  • Un controller-manager que reconcilia estado deseado contra estado real.
  • Un kubelet que materializa Pods en cada worker.
  • Un runtime CRI, normalmente containerd.
  • Un CNI que hace posible la red de Pods.
  • Un DNS interno que vuelve usable el cluster.

Ese recorrido sirve para aprender, depurar y endurecer. No es el camino mas comodo para produccion, ni pretende serlo. En produccion normalmente conviene usar herramientas con ciclo de vida, upgrades, validaciones y automatizacion. Pero si alguna vez has visto un nodo NotReady, un Service sin endpoints, un certificado con SAN incorrecto o un etcd sin quorum, este laboratorio te da el mapa mental para no depurar a ciegas.

Versiones y alcance

Al 25 de abril de 2026, la serie activa mas reciente de Kubernetes es 1.36, con 1.36.0 publicada el 22 de abril de 2026. Eso no significa que debas usar siempre "lo ultimo" en un laboratorio manual. En un hard way conviene fijar explicitamente minor y patch, y mantener control plane, kubelet y kube-proxy dentro de la politica oficial de skew de versiones.

Una base razonable para un laboratorio seria:

export K8S_VERSION="v1.36.0"
export ETCD_VERSION="<pin_compatible_con_tu_minor>"
export CONTAINERD_VERSION="<pin_actual_validado>"
export RUNC_VERSION="<pin_actual_validado>"
export CNI_PLUGINS_VERSION="<pin_actual_validado>"

Dejo etcd, containerd, runc y CNI como placeholders a proposito. El habito correcto es fijarlos en un .env, Ansible, Terraform o documentacion operativa. En un cluster montado a mano, depender de latest es una invitacion a errores dificiles de reproducir.

Esta guia asume Linux con systemd, SSH, openssl, curl, jq, kubectl y comodidad editando YAML y units de systemd. No asume una distro concreta, nube publica ni numero fijo de nodos.

Topologia recomendada

Antes de tocar comandos, decide la topologia. El diseno mas pequeno sirve para aprender; el diseno HA sirve para entender quorum, balanceo y fallos reales.

| Topologia | Que ganas | Que pierdes | Cuando usarla | |---|---|---|---| | 1 control plane + etcd local | Montaje simple y didactico | Sin quorum; API y estado caen juntos | Aprendizaje, demos, laboratorio efimero | | 3 control planes + 3 miembros etcd | Toleras la caida de 1 miembro | Mas certificados, red y operacion | HA razonable en bare metal o VMs | | 3 o 5 control planes + 5 miembros etcd | Mas tolerancia a fallos | Mas latencia de escritura y complejidad | Produccion madura con disciplina operativa |

etcd no escala por agregar miembros para "tener mas capacidad". Agregar miembros mejora disponibilidad a costa de latencia y complejidad. Para un laboratorio serio, 3 miembros suele ser el punto de equilibrio.

Inventario base

Declara el entorno antes de instalar nada. Esta claridad evita errores con SANs, kubeconfigs, rutas y rangos superpuestos.

cat > inventory.env <<'EOF'
CLUSTER_NAME="kthw-lab"
CLUSTER_DOMAIN="cluster.local"

API_ENDPOINT_FQDN="k8s-api.lab.local"
API_ENDPOINT_PORT="6443"
API_ENDPOINT="${API_ENDPOINT_FQDN}:${API_ENDPOINT_PORT}"

POD_CIDR="10.200.0.0/16"
SERVICE_CIDR="10.32.0.0/24"
DNS_SERVICE_IP="10.32.0.10"

CP1_NAME="cp1"
CP1_IP="10.0.0.11"

WK1_NAME="wk1"
WK1_IP="10.0.0.21"
WK2_NAME="wk2"
WK2_IP="10.0.0.22"

CP2_NAME="cp2"
CP2_IP="10.0.0.12"
CP3_NAME="cp3"
CP3_IP="10.0.0.13"
EOF

source inventory.env

Los rangos de Nodes, Pods y Services no deben solaparse. En este ejemplo, 10.200.0.0/16 identifica Pods y 10.32.0.0/24 identifica Services. Lo importante no es copiar esos CIDRs exactos, sino mantenerlos separados y usarlos de forma coherente en API server, controller-manager, kube-proxy y CNI.

Orden de trabajo

El orden importa:

  1. Inventario y versiones.
  2. PKI y kubeconfigs.
  3. etcd.
  4. kube-apiserver.
  5. kube-controller-manager.
  6. kube-scheduler.
  7. containerd en workers.
  8. kubelet.
  9. kube-proxy.
  10. CNI.
  11. CoreDNS.
  12. RBAC, smoke tests, backups y troubleshooting.

Los workers necesitan un API estable. El API necesita etcd. CoreDNS necesita red. kubelet y kube-proxy necesitan kubeconfigs y certificados. Si te saltas ese orden, los errores empiezan a mezclarse.

PKI: la identidad del cluster

La parte mas importante de un hard way moderno no es el YAML: es la identidad. Kubernetes autentica componentes con certificados cliente, tokens bearer o proxys autenticantes. En este laboratorio, la ruta principal usa certificados.

Principios basicos:

  • No uses system:masters para usuarios humanos normales. Ese grupo evita restricciones de autorizacion.
  • Para administracion cotidiana, crea un grupo propio y ligalo a cluster-admin con RBAC.
  • Para kubelets, respeta el formato CN=system:node:<nodeName> y O=system:nodes.
  • El nodeName del certificado debe coincidir con el nombre que registra kubelet.
  • El API server debe verificar al kubelet con --kubelet-certificate-authority.

Artefactos minimos:

| Artefacto | Uso | Subject recomendado | |---|---|---| | ca.crt / ca.key | CA principal de Kubernetes | CN=kubernetes-ca | | etcd-ca.crt / etcd-ca.key | CA separada de etcd | CN=etcd-ca | | sa.key / sa.pub | Firma de ServiceAccount tokens | Par de claves | | apiserver.crt | TLS del kube-apiserver | CN=kube-apiserver + SANs | | apiserver-kubelet-client.crt | API server hacia kubelet | CN=kube-apiserver-kubelet-client | | apiserver-etcd-client.crt | API server hacia etcd | CN=kube-apiserver-etcd-client | | controller-manager.crt | kube-controller-manager | CN=system:kube-controller-manager | | scheduler.crt | kube-scheduler | CN=system:kube-scheduler | | kube-proxy.crt | kube-proxy | CN=system:kube-proxy | | wk1-kubelet.crt | kubelet de un worker | CN=system:node:wk1, O=system:nodes | | etcd-server.crt | servidor etcd | SANs del nodo etcd | | etcd-peer.crt | peer etcd | SANs del nodo etcd | | etcd-healthcheck-client.crt | etcdctl con mTLS | CN=kube-etcd-healthcheck-client |

Genera las CAs y claves de ServiceAccount:

mkdir -p ~/kthw/{pki,kubeconfigs,configs,units}
cd ~/kthw/pki

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=kubernetes-ca" \
  -out ca.crt

openssl genrsa -out etcd-ca.key 4096
openssl req -x509 -new -nodes -key etcd-ca.key -sha256 -days 3650 \
  -subj "/CN=etcd-ca" \
  -out etcd-ca.crt

openssl genrsa -out sa.key 4096
openssl rsa -in sa.key -pubout -out sa.pub

Para un usuario admin, evita system:masters:

openssl genrsa -out admin.key 4096
openssl req -new -key admin.key \
  -subj "/CN=kubernetes-admin/O=kthw:admins" \
  -out admin.csr

Para un kubelet:

NODE_NAME="wk1"

openssl genrsa -out ${NODE_NAME}-kubelet.key 4096
openssl req -new -key ${NODE_NAME}-kubelet.key \
  -subj "/CN=system:node:${NODE_NAME}/O=system:nodes" \
  -out ${NODE_NAME}-kubelet.csr

cat > ${NODE_NAME}-kubelet.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth,serverAuth
EOF

openssl x509 -req -in ${NODE_NAME}-kubelet.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out ${NODE_NAME}-kubelet.crt -days 365 -sha256 \
  -extfile ${NODE_NAME}-kubelet.ext

El certificado del API server es el que mas falla en laboratorios. Debe incluir el endpoint real, los nombres internos del Service kubernetes y la primera IP del rango de Services:

cat > apiserver.ext <<EOF
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=@alt_names

[alt_names]
DNS.1=${API_ENDPOINT_FQDN}
DNS.2=kubernetes
DNS.3=kubernetes.default
DNS.4=kubernetes.default.svc
DNS.5=kubernetes.default.svc.${CLUSTER_DOMAIN}
IP.1=${CP1_IP}
IP.2=10.32.0.1
EOF

En HA, agrega el VIP o balanceador y las IPs de cada control plane.

Kubeconfigs

Un kubeconfig contiene endpoints, CAs e identidad. Tratalo como secreto. Para generar kubeconfigs repetibles:

cd ~/kthw

make_kubeconfig() {
  local NAME="$1"
  local USER="$2"
  local CRT="$3"
  local KEY="$4"

  kubectl config set-cluster "${CLUSTER_NAME}" \
    --certificate-authority="${HOME}/kthw/pki/ca.crt" \
    --embed-certs=true \
    --server="https://${API_ENDPOINT}" \
    --kubeconfig="${HOME}/kthw/kubeconfigs/${NAME}.kubeconfig"

  kubectl config set-credentials "${USER}" \
    --client-certificate="${CRT}" \
    --client-key="${KEY}" \
    --embed-certs=true \
    --kubeconfig="${HOME}/kthw/kubeconfigs/${NAME}.kubeconfig"

  kubectl config set-context default \
    --cluster="${CLUSTER_NAME}" \
    --user="${USER}" \
    --kubeconfig="${HOME}/kthw/kubeconfigs/${NAME}.kubeconfig"

  kubectl config use-context default \
    --kubeconfig="${HOME}/kthw/kubeconfigs/${NAME}.kubeconfig"
}

Ejemplos:

make_kubeconfig admin kubernetes-admin \
  ~/kthw/pki/admin.crt \
  ~/kthw/pki/admin.key

make_kubeconfig scheduler system:kube-scheduler \
  ~/kthw/pki/scheduler.crt \
  ~/kthw/pki/scheduler.key

make_kubeconfig kube-proxy system:kube-proxy \
  ~/kthw/pki/kube-proxy.crt \
  ~/kthw/pki/kube-proxy.key

make_kubeconfig wk1 system:node:wk1 \
  ~/kthw/pki/wk1-kubelet.crt \
  ~/kthw/pki/wk1-kubelet.key

RBAC minimo

Un arranque limpio necesita permisos explicitos:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kthw-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: Group
  name: kthw:admins
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system-kube-proxy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:node-proxier
subjects:
- kind: User
  name: system:kube-proxy
  apiGroup: rbac.authorization.k8s.io

Ese primer binding da poder administrativo a tu grupo, no a un usuario metido en system:masters. El segundo permite que kube-proxy observe Services y Endpoints para programar reglas de red.

Cifrado en reposo

TLS protege en transito. Para Secrets en etcd, configura cifrado en reposo desde el API server:

ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

cat > ~/kthw/configs/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: ${ENCRYPTION_KEY}
  - identity: {}
EOF

Luego pasa ese archivo al API server con --encryption-provider-config.

etcd: la memoria del cluster

etcd es la fuente de verdad persistente. Si etcd se pierde, Kubernetes pierde memoria operacional: Deployments, Secrets, Services, objetos de estado y mucho mas.

Instalacion base en cada nodo etcd:

install -m 0755 etcd etcdctl etcdutl /usr/local/bin/

useradd --system --home /var/lib/etcd --shell /sbin/nologin etcd || true
mkdir -p /etc/etcd/pki /var/lib/etcd
chown -R etcd:etcd /var/lib/etcd /etc/etcd
chmod 0700 /var/lib/etcd

Ejemplo single-node para /etc/etcd/etcd.env:

ETCD_NAME=cp1
ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-01
ETCD_INITIAL_CLUSTER_STATE=new
ETCD_INITIAL_CLUSTER=cp1=https://10.0.0.11:2380

ETCD_LISTEN_CLIENT_URLS=https://127.0.0.1:2379,https://10.0.0.11:2379
ETCD_ADVERTISE_CLIENT_URLS=https://10.0.0.11:2379

ETCD_LISTEN_PEER_URLS=https://10.0.0.11:2380
ETCD_INITIAL_ADVERTISE_PEER_URLS=https://10.0.0.11:2380

Unidad systemd:

[Unit]
Description=etcd
Documentation=https://etcd.io
After=network-online.target
Wants=network-online.target

[Service]
User=etcd
Type=notify
EnvironmentFile=/etc/etcd/etcd.env
ExecStart=/usr/local/bin/etcd \
  --name=${ETCD_NAME} \
  --data-dir=/var/lib/etcd \
  --listen-client-urls=${ETCD_LISTEN_CLIENT_URLS} \
  --advertise-client-urls=${ETCD_ADVERTISE_CLIENT_URLS} \
  --listen-peer-urls=${ETCD_LISTEN_PEER_URLS} \
  --initial-advertise-peer-urls=${ETCD_INITIAL_ADVERTISE_PEER_URLS} \
  --initial-cluster=${ETCD_INITIAL_CLUSTER} \
  --initial-cluster-state=${ETCD_INITIAL_CLUSTER_STATE} \
  --initial-cluster-token=${ETCD_INITIAL_CLUSTER_TOKEN} \
  --cert-file=/etc/etcd/pki/server.crt \
  --key-file=/etc/etcd/pki/server.key \
  --client-cert-auth \
  --trusted-ca-file=/etc/etcd/pki/etcd-ca.crt \
  --peer-cert-file=/etc/etcd/pki/peer.crt \
  --peer-key-file=/etc/etcd/pki/peer.key \
  --peer-client-cert-auth \
  --peer-trusted-ca-file=/etc/etcd/pki/etcd-ca.crt
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Arranque:

systemctl daemon-reload
systemctl enable --now etcd
systemctl status etcd --no-pager

Verifica salud:

export ETCDCTL_API=3

etcdctl \
  --endpoints="https://10.0.0.11:2379" \
  --cacert=/etc/etcd/pki/etcd-ca.crt \
  --cert=/etc/etcd/pki/healthcheck-client.crt \
  --key=/etc/etcd/pki/healthcheck-client.key \
  endpoint health -w table

etcdctl \
  --endpoints="https://10.0.0.11:2379" \
  --cacert=/etc/etcd/pki/etcd-ca.crt \
  --cert=/etc/etcd/pki/healthcheck-client.crt \
  --key=/etc/etcd/pki/healthcheck-client.key \
  endpoint status -w table

Control plane

El control plane minimo tiene tres componentes:

  • kube-apiserver: frontal REST, autenticacion, autorizacion, admision y persistencia.
  • kube-controller-manager: control loops que reconcilian estado.
  • kube-scheduler: asignacion de Pods pendientes a nodos.

Una unidad explicita de kube-apiserver:

[Unit]
Description=Kubernetes API Server
Documentation=https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
After=network-online.target etcd.service
Wants=network-online.target
Requires=etcd.service

[Service]
ExecStart=/usr/local/bin/kube-apiserver \
  --advertise-address=10.0.0.11 \
  --allow-privileged=true \
  --anonymous-auth=false \
  --authorization-mode=Node,RBAC \
  --bind-address=0.0.0.0 \
  --client-ca-file=/etc/kubernetes/pki/ca.crt \
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
  --encryption-provider-config=/etc/kubernetes/encryption/encryption-config.yaml \
  --etcd-cafile=/etc/kubernetes/pki/etcd-ca.crt \
  --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt \
  --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key \
  --etcd-servers=https://10.0.0.11:2379 \
  --event-ttl=1h \
  --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt \
  --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \
  --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \
  --secure-port=6443 \
  --service-account-issuer=https://k8s-api.lab.local:6443 \
  --service-account-key-file=/etc/kubernetes/pki/sa.pub \
  --service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
  --service-cluster-ip-range=10.32.0.0/24 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
  --tls-private-key-file=/etc/kubernetes/pki/apiserver.key \
  --v=2
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

La unidad de kube-controller-manager debe llevar el cluster-cidr, el rango de Services, la CA para firmar CSRs si activas rotacion y la clave privada de ServiceAccount:

ExecStart=/usr/local/bin/kube-controller-manager \
  --allocate-node-cidrs=true \
  --authentication-kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \
  --authorization-kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \
  --bind-address=127.0.0.1 \
  --cluster-cidr=10.200.0.0/16 \
  --cluster-name=kthw-lab \
  --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt \
  --cluster-signing-key-file=/etc/kubernetes/pki/ca.key \
  --kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \
  --leader-elect=true \
  --root-ca-file=/etc/kubernetes/pki/ca.crt \
  --service-account-private-key-file=/etc/kubernetes/pki/sa.key \
  --service-cluster-ip-range=10.32.0.0/24 \
  --use-service-account-credentials=true \
  --v=2

El scheduler moderno funciona bien con un archivo de configuracion:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/scheduler.kubeconfig
leaderElection:
  leaderElect: true
profiles:
- schedulerName: default-scheduler

Si el control plane no queda sano, no sigas con workers. Revisa primero:

journalctl -u etcd -xe --no-pager
journalctl -u kube-apiserver -xe --no-pager
journalctl -u kube-controller-manager -xe --no-pager
journalctl -u kube-scheduler -xe --no-pager

Workers: runtime, kubelet y kube-proxy

Cada worker necesita tres cosas: runtime CRI, kubelet y kube-proxy. En esta guia uso containerd porque es comun, directo y encaja bien con Kubernetes moderno.

Preflight del host:

cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

cat > /etc/sysctl.d/99-kubernetes.conf <<'EOF'
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

sysctl --system

Configuracion minima de containerd:

version = 2

[plugins."io.containerd.grpc.v1.cri"]
  [plugins."io.containerd.grpc.v1.cri".containerd]
    snapshotter = "overlayfs"
    default_runtime_name = "runc"

  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"

  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
    SystemdCgroup = true

  [plugins."io.containerd.grpc.v1.cri".cni]
    bin_dir = "/opt/cni/bin"
    conf_dir = "/etc/cni/net.d"

Kubelet:

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: /var/lib/kubelet/ca.crt
authorization:
  mode: Webhook
cgroupDriver: systemd
clusterDNS:
- 10.32.0.10
clusterDomain: cluster.local
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock
readOnlyPort: 0
registerNode: true
tlsCertFile: /var/lib/kubelet/kubelet.crt
tlsPrivateKeyFile: /var/lib/kubelet/kubelet.key

Unidad:

ExecStart=/usr/local/bin/kubelet \
  --config=/var/lib/kubelet/kubelet-config.yaml \
  --hostname-override=wk1 \
  --kubeconfig=/var/lib/kubelet/kubeconfig \
  --node-ip=10.0.0.21 \
  --v=2

kube-proxy:

kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: /var/lib/kube-proxy/kubeconfig
mode: "iptables"
clusterCIDR: "10.200.0.0/16"

Uso iptables por claridad y compatibilidad. En Linux tambien existen modos como nftables; ipvs ya no es la recomendacion principal para laboratorios nuevos.

CNI: Calico o Flannel

Un cluster sin CNI no esta completo. kubelet puede registrar nodos, pero los Pods no tendran red funcional.

| CNI | Que ofrece | Limite | Mejor encaje | |---|---|---|---| | Calico | CNI, IPAM, rutas/overlay y NetworkPolicy | Mas piezas y decisiones | Clusters donde quieres seguridad de red | | Flannel | Red L3 simple y facil | No implementa NetworkPolicy por si solo | Laboratorio y simplicidad didactica |

Con Calico:

curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.31.4/manifests/calico.yaml

# Si tu POD_CIDR no coincide con el default del manifest, ajusta CALICO_IPV4POOL_CIDR.
kubectl apply -f calico.yaml

Con Flannel:

curl -LO https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
sed -i 's#10.244.0.0/16#10.200.0.0/16#g' kube-flannel.yml
kubectl apply -f kube-flannel.yml

El punto didactico es entender POD_CIDR, SERVICE_CIDR, rutas, encapsulacion y politicas. En un hard way historico se hacian rutas a mano; en un hard way moderno, el CNI suele tomar esa responsabilidad.

DNS con CoreDNS

Kubernetes usa DNS para que los Services sean usables. El Service se llama kube-dns por compatibilidad, aunque el componente recomendado sea CoreDNS.

Si tu SERVICE_CIDR es 10.32.0.0/24, reserva 10.32.0.10:

apiVersion: v1
kind: Service
metadata:
  name: kube-dns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/name: "CoreDNS"
spec:
  clusterIP: 10.32.0.10
  selector:
    k8s-app: kube-dns
  ports:
  - name: dns
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53

Despues de aplicar CoreDNS, valida con:

kubectl apply -f https://k8s.io/examples/admin/dns/dnsutils.yaml
kubectl get pod dnsutils
kubectl exec -it dnsutils -- nslookup kubernetes.default

Smoke tests que importan

No basta con ver nodos Ready. Prueba control plane, red, Services, DNS, logs y exec:

export KUBECONFIG=~/kthw/kubeconfigs/admin.kubeconfig

kubectl version
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl cluster-info

kubectl create namespace smoke
kubectl -n smoke create deployment nginx --image=registry.k8s.io/nginx:stable
kubectl -n smoke expose deployment nginx --port=80 --target-port=80
kubectl -n smoke get pods,svc -o wide

Si esto funciona, el cluster no solo arranco: las piezas cooperan.

Backup y restore de etcd

No dejes los backups para "despues". etcd documenta snapshots con etcdctl snapshot save y validacion con etcdutl snapshot status.

export ETCDCTL_API=3
BACKUP_FILE="/var/backups/etcd-$(date +%F-%H%M%S).db"

etcdctl \
  --endpoints="https://10.0.0.11:2379" \
  --cacert=/etc/etcd/pki/etcd-ca.crt \
  --cert=/etc/etcd/pki/healthcheck-client.crt \
  --key=/etc/etcd/pki/healthcheck-client.key \
  snapshot save "${BACKUP_FILE}"

etcdutl snapshot status "${BACKUP_FILE}" -w table

Para restaurar single-node:

systemctl stop etcd
mv /var/lib/etcd /var/lib/etcd.broken.$(date +%s)

etcdutl snapshot restore /var/backups/etcd-2026-04-25-120000.db \
  --name cp1 \
  --initial-cluster cp1=https://10.0.0.11:2380 \
  --initial-cluster-token etcd-cluster-restore-01 \
  --initial-advertise-peer-urls https://10.0.0.11:2380 \
  --data-dir /var/lib/etcd

chown -R etcd:etcd /var/lib/etcd
systemctl start etcd

En un cluster de 3 miembros, restaura los tres desde el mismo snapshot con nombres, peer URLs y initial-cluster coherentes. No mezcles un miembro restaurado con dos miembros viejos esperando consistencia.

Troubleshooting por plano

La forma correcta de depurar es preguntar que plano se rompio: identidad, persistencia, networking o servicios.

| Sintoma | Sospecha principal | Que mirar primero | |---|---|---| | kubectl no conecta | SAN del API, CA equivocada, API caido | journalctl -u kube-apiserver, certificado del API | | Worker NotReady | CN/O del kubelet, hostname, kubeconfig | journalctl -u kubelet, subject del cert | | kubectl logs o exec falla | Ruta API server hacia kubelet | flags --kubelet-*, logs del API server | | Service no responde | kube-proxy o Endpoints | kubectl get endpointslice, iptables-save | | DNS no resuelve | CoreDNS, Service kube-dns, RBAC | logs de CoreDNS y endpoints | | Pods sin red | CNI ausente o CIDR incorrecto | /opt/cni/bin, logs de Calico/Flannel | | etcd sin quorum | peer URLs, certs peer, initial-cluster | etcdctl endpoint health/status |

Comandos que conviene memorizar:

journalctl -u etcd -xe --no-pager
journalctl -u kube-apiserver -xe --no-pager
journalctl -u kube-controller-manager -xe --no-pager
journalctl -u kube-scheduler -xe --no-pager
journalctl -u containerd -xe --no-pager
journalctl -u kubelet -xe --no-pager
journalctl -u kube-proxy -xe --no-pager

kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl describe node wk1
kubectl get events -A --sort-by=.lastTimestamp
kubectl get svc,endpoints,endpointslice -A

Como lo usaria para aprender

Yo lo haria en tres vueltas:

  1. Completar la guia sin optimizar nada.
  2. Repetirla documentando cada comando con mis propias palabras.
  3. Automatizar pequenas partes solo despues de poder explicarlas sin mirar la receta.

Ese orden evita aprender herramientas antes que fundamentos. Kubernetes The Hard Way no compite con la automatizacion. La vuelve mas comprensible.

Checklist final

  • Entender que certificado usa cada componente.
  • Saber que guarda etcd y como se respalda.
  • Poder explicar por que el API server es la frontera del sistema.
  • Reconocer que hace scheduler y que hace controller-manager.
  • Entender el papel de kubelet, containerd y kube-proxy.
  • Instalar un CNI y explicar que problema resuelve.
  • Probar DNS, Services, logs, exec y Secrets.
  • Depurar fallos separando identidad, persistencia, red y servicios.

El resultado mas valioso no es el cluster. Es la capacidad de diagnosticarlo.

Fuentes y lectura recomendada

E

Escrito por

Edgar

Community

Login to like and comment.

Comments (0)

No comments yet.