Esta es la última lección del curso y funciona de forma distinta a todas las anteriores: no se lee, se hace. Es un examen de prácticas completo, con quince tareas cronometradas al estilo de las certificaciones, sobre la plataforma Rutas Norte que has construido a lo largo de los once módulos anteriores. Tiene su puntuación, su solucionario y una tabla de autoevaluación que traduce tu resultado en un veredicto claro: listo para reservar fecha, repasar dos dominios concretos, o volver a determinados módulos.

La regla más importante: haz el simulacro antes de leer las soluciones. El valor de esta lección no está en el solucionario —está en descubrir, con un cronómetro corriendo, qué sabes hacer sin ayuda y qué no. Leerlo primero convierte un diagnóstico en una lectura agradable y sin utilidad.

Prepara el clúster, pon el cronómetro a 120 minutos y empieza.

Contenido

  1. Instrucciones del simulacro
  2. Preparación del entorno
  3. Las quince tareas
  4. Reparto del tiempo y estrategia

  1. Instrucciones del simulacro

1.1 Reglas

Estas reglas replican las condiciones reales del examen. Respetarlas es lo que hace que el resultado signifique algo.

Regla Detalle
Tiempo total 120 minutos, cronometrados desde la primera tarea
Sin pausas Nada de parar el reloj. Ve al baño antes.
Documentación Solo kubernetes.io/docs. Una pestaña. Nada más.
Prohibido Buscadores, foros, GitHub, notas propias, esta lección, herramientas de IA
Editor vim o nano en el terminal. No un IDE gráfico.
Móvil En otra habitación
Puntuación 100 puntos repartidos entre las 15 tareas
Corrección Al final, con el solucionario. Nunca durante.

1.2 Cómo puntuar

Cada tarea indica su criterio de aceptación. Puntúa así:

  • Puntuación completa si se cumple todo el criterio.
  • Puntuación parcial (la mitad) si se cumple una parte sustancial pero falta algún requisito.
  • Cero si el objeto no existe, está en el namespace equivocado o no funciona.

Sé honesto contigo mismo. Inflar la nota solo te perjudica.

1.3 Qué anotar mientras trabajas

Ten un fichero de notas abierto (o papel, aquí sí vale) y registra para cada tarea:

T1  5p  ✔  3 min
T2  7p  ½  9 min   - no me salió el Ingress a la primera
T3  6p  ✔  4 min
T4  8p  ⏸  saltada - PVC en Pending y no supe por qué
...

Esta información es tan valiosa como la nota final: te dice dónde se te va el tiempo.


  1. Preparación del entorno

2.1 Opción recomendada: kind con tres nodos

kind levanta un clúster multinodo en un minuto y soporta casi todo lo que necesita el simulacro. Necesitas Docker o Podman instalado.

# kind-simulacro.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30080
    hostPort: 30080
- role: worker
- role: worker
networking:
  disableDefaultCNI: true          # instalaremos Calico para NetworkPolicies
kind create cluster --name simulacro --config kind-simulacro.yaml

# CNI con soporte de NetworkPolicies (imprescindible para la tarea 9)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml

# Esperar a que todos los nodos estén Ready
kubectl wait --for=condition=Ready nodes --all --timeout=300s
kubectl get nodes
NAME                      STATUS   ROLES           AGE   VERSION
simulacro-control-plane   Ready    control-plane   2m    v1.30.0
simulacro-worker          Ready    <none>          2m    v1.30.0
simulacro-worker2         Ready    <none>          2m    v1.30.0

2.2 Alternativa: minikube

minikube start --nodes=3 --cni=calico --kubernetes-version=v1.30.0
minikube addons enable ingress
minikube addons enable metrics-server

Minikube tiene la ventaja de traer el controlador de Ingress y el metrics-server como addons, que en kind hay que instalar aparte.

2.3 Componentes adicionales

Para que todas las tareas sean resolubles, instala esto antes de arrancar el cronómetro:

# Controlador de Ingress (necesario para la tarea 2)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/kind/deploy.yaml
kubectl wait --namespace ingress-nginx --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller --timeout=180s

# metrics-server (necesario para la tarea 11)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch deployment metrics-server -n kube-system --type='json' \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

2.4 Script de preparación (ejecútalo antes del cronómetro)

Este script crea los namespaces, las cargas de partida y las tres averías de las tareas de resolución de problemas. Guárdalo como preparar-simulacro.sh y ejecútalo. No lo leas con detalle: las averías deben ser una sorpresa.

#!/bin/bash
set -e

for ns in rutas-norte-dev rutas-norte-pre rutas-norte-pro; do
  kubectl create namespace $ns --dry-run=client -o yaml | kubectl apply -f -
done

# Etiquetas de nodo para la tarea 5
kubectl label node simulacro-worker2 tipo=batch --overwrite
kubectl taint node simulacro-worker2 dedicado=batch:NoSchedule --overwrite

# Carga de partida para las tareas 7, 11 y 12
kubectl create deployment api-reservas --image=nginx:1.27-alpine \
  --replicas=2 -n rutas-norte-pre
kubectl create deployment tienda-web --image=nginx:1.27-alpine \
  --replicas=3 -n rutas-norte-pro

# --- AVERÍA 1 (tarea 13) ---
kubectl create deployment worker-notificaciones --image=busybox:1.36 \
  -n rutas-norte-dev
kubectl set env deployment/worker-notificaciones -n rutas-norte-dev \
  --from=secret/credenciales-cola --prefix=COLA_ 2>/dev/null || \
kubectl patch deployment worker-notificaciones -n rutas-norte-dev --type='json' \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/envFrom",
       "value":[{"secretRef":{"name":"credenciales-cola"}}]}]'

# --- AVERÍA 2 (tarea 14) ---
kubectl create deployment redis-cache --image=redis:7-alpine -n rutas-norte-pre
kubectl expose deployment redis-cache --name=redis-cache-svc --port=6379 \
  -n rutas-norte-pre
kubectl patch svc redis-cache-svc -n rutas-norte-pre \
  -p '{"spec":{"selector":{"app":"redis"}}}'

# --- AVERÍA 3 (tarea 15) ---
kubectl run informes-ocupacion --image=busybox:1.36 -n rutas-norte-pro \
  --overrides='{"spec":{"nodeSelector":{"disco":"nvme"}}}' \
  --command -- sleep 3600

echo "Entorno preparado. Arranca el cronómetro."
chmod +x preparar-simulacro.sh
./preparar-simulacro.sh

2.5 Ahora sí: arranca el cronómetro

Antes de leer la tarea 1, teclea tu bloque de arranque (lección 12-04):

alias k=kubectl
export do='--dry-run=client -o yaml'
export now='--force --grace-period=0'
source <(kubectl completion bash)
complete -o default -F __start_kubectl k

cat <<'EOF' > ~/.vimrc
set number
set expandtab
set tabstop=2
set shiftwidth=2
set softtabstop=2
set autoindent
set paste
EOF

120 minutos. Empieza.


  1. Las quince tareas


Tarea 1 — Despliegue básico y escalado

Namespace: rutas-norte-dev · Peso: 5 puntos · Objetivo: 4 min

Crea un Deployment llamado tienda-web con la imagen nginx:1.27-alpine y 4 réplicas. Todos sus pods deben llevar la etiqueta entorno=dev además de las que ponga el controlador. Después, escálalo a 6 réplicas.

Criterio de aceptación: kubectl get deploy tienda-web -n rutas-norte-dev muestra 6/6 y todos los pods tienen la etiqueta entorno=dev.


Tarea 2 — Exposición: Service e Ingress

Namespace: rutas-norte-dev · Peso: 7 puntos · Objetivo: 8 min

Expón el Deployment tienda-web de la tarea anterior con un Service ClusterIP llamado tienda-web-svc en el puerto 80. Crea además un Ingress tienda-ingress con la IngressClass nginx que enrute el host www.rutasnorte.es, ruta /, hacia ese Service en el puerto 80.

Criterio de aceptación: el Service tiene endpoints poblados y kubectl describe ingress tienda-ingress muestra la regla con el backend correcto.


Tarea 3 — Configuración y secretos

Namespace: rutas-norte-dev · Peso: 6 puntos · Objetivo: 6 min

Crea:

  • Un ConfigMap config-api con las claves nivel_log=info y max_conexiones=100.
  • Un Secret credenciales-postgres con usuario=reservas y password=N0rt3-2026.

Crea un Pod api-reservas con la imagen nginx:1.27-alpine que reciba todas las claves del ConfigMap como variables de entorno y monte el Secret como ficheros en /etc/secretos en modo solo lectura.

Criterio de aceptación: kubectl exec api-reservas -- env | grep nivel_log devuelve nivel_log=info y kubectl exec api-reservas -- cat /etc/secretos/usuario devuelve reservas.


Tarea 4 — Almacenamiento persistente

Namespace: rutas-norte-pre · Peso: 8 puntos · Objetivo: 9 min

Crea un PersistentVolume pv-reservas de 3Gi, modo de acceso ReadWriteOnce, storageClassName: manual, política de reclamación Retain, con hostPath en /mnt/datos-reservas.

Crea un PersistentVolumeClaim pvc-reservas de 2Gi que se enlace a ese PV, y un Pod postgres-reservas con imagen busybox:1.36 que ejecute sleep 3600 y monte el volumen en /var/lib/datos.

Criterio de aceptación: el PVC aparece como Bound al PV pv-reservas y el pod está Running con el volumen montado.


Tarea 5 — Planificación con taints y etiquetas de nodo

Namespace: rutas-norte-pro · Peso: 6 puntos · Objetivo: 6 min

El nodo simulacro-worker2 tiene el taint dedicado=batch:NoSchedule y la etiqueta tipo=batch.

Crea un Deployment worker-notificaciones con imagen busybox:1.36, comando sleep 3600, 2 réplicas, que se ejecute exclusivamente en ese nodo.

Criterio de aceptación: los 2 pods están Running en simulacro-worker2 (compruébalo con -o wide).


Tarea 6 — Trabajo programado

Namespace: rutas-norte-pro · Peso: 5 puntos · Objetivo: 5 min

Crea un CronJob informes-nocturnos que se ejecute cada día a las 2:30, con imagen busybox:1.36 y comando echo informe de ocupacion generado. Requisitos: no debe permitir ejecuciones solapadas, cada Job puede reintentar como máximo 2 veces, y solo se conservan 3 Jobs exitosos en el historial.

Criterio de aceptación: el CronJob existe con schedule: "30 2 * * *", concurrencyPolicy: Forbid, backoffLimit: 2 y successfulJobsHistoryLimit: 3. Una ejecución manual produce la salida esperada.


Tarea 7 — Sondas y calidad de servicio

Namespace: rutas-norte-pre · Peso: 7 puntos · Objetivo: 7 min

El Deployment api-reservas ya existe. Modifícalo para que:

  • No reciba tráfico hasta que responda GET / en el puerto 80 (readinessProbe).
  • Se reinicie si GET / falla 3 veces seguidas (livenessProbe).
  • Tenga clase de QoS Guaranteed con 200m de CPU y 256Mi de memoria.

Criterio de aceptación: kubectl describe pod -l app=api-reservas -n rutas-norte-pre muestra QoS Class: Guaranteed y ambas sondas configuradas.


Tarea 8 — RBAC y ServiceAccount

Namespace: rutas-norte-pro · Peso: 7 puntos · Objetivo: 7 min

Crea la ServiceAccount soporte-n1 en rutas-norte-pro. Concédele permiso para listar y ver pods y leer sus logs, únicamente en ese namespace. No debe poder borrar nada ni acceder a otros namespaces.

Asigna esa ServiceAccount al Deployment tienda-web de rutas-norte-pro.

Criterio de aceptación: kubectl auth can-i list pods --as=system:serviceaccount:rutas-norte-pro:soporte-n1 -n rutas-norte-pro devuelve yes; la misma consulta con delete o en otro namespace devuelve no.


Tarea 9 — Política de red

Namespace: rutas-norte-pro · Peso: 7 puntos · Objetivo: 8 min

Crea en rutas-norte-pro una NetworkPolicy llamada denegar-todo-entrada que deniegue todo el tráfico de entrada a todos los pods del namespace.

Añade una segunda política permitir-tienda-desde-ingress que permita el tráfico de entrada al puerto 80 de los pods con etiqueta app=tienda-web únicamente desde pods del namespace ingress-nginx.

Criterio de aceptación: ambas políticas existen; un pod de prueba dentro de rutas-norte-pro no puede alcanzar a tienda-web en el puerto 80.


Tarea 10 — Endurecimiento y admisión

Namespace: rutas-norte-pre · Peso: 7 puntos · Objetivo: 7 min

Etiqueta el namespace rutas-norte-pre para que aplique el estándar restricted de Pod Security Admission en modo enforce, con la versión v1.30.

Después, crea un Pod auditor-seguro (imagen busybox:1.36, comando sleep 3600) que cumpla ese estándar: usuario no root, sin escalada de privilegios, todas las capacidades descartadas y perfil seccomp por defecto.

Criterio de aceptación: el namespace tiene la etiqueta de enforce; el pod auditor-seguro está Running; un kubectl run inseguro --image=nginx -n rutas-norte-pre es rechazado.


Tarea 11 — Autoescalado

Namespace: rutas-norte-pre · Peso: 5 puntos · Objetivo: 4 min

Crea un HorizontalPodAutoscaler para el Deployment api-reservas que mantenga entre 2 y 8 réplicas, con un objetivo de utilización de CPU del 65 %.

Criterio de aceptación: kubectl get hpa -n rutas-norte-pre muestra el HPA con MINPODS 2, MAXPODS 8 y el objetivo del 65 %, y con métricas leídas (no <unknown>).


Tarea 12 — Actualización y reversión

Namespace: rutas-norte-pro · Peso: 6 puntos · Objetivo: 6 min

Sobre el Deployment tienda-web de rutas-norte-pro:

  1. Configura la estrategia para que ningún pod deje de estar disponible durante la actualización (maxUnavailable: 0, maxSurge: 1).
  2. Actualiza la imagen a nginx:1.27.2-alpine y anota la causa del cambio como subida a 1.27.2.
  3. Espera a que termine el despliegue y después revierte a la revisión anterior.

Criterio de aceptación: kubectl rollout history muestra al menos dos revisiones con la causa anotada, y la imagen final vuelve a ser nginx:1.27-alpine.


Tarea 13 — Resolución de problemas: el worker que no arranca

Namespace: rutas-norte-dev · Peso: 8 puntos · Objetivo: 8 min

El Deployment worker-notificaciones de rutas-norte-dev no consigue arrancar ningún pod. Encuentra la causa, corrígela sin cambiar la imagen ni la configuración del contenedor, y deja el Deployment con al menos un pod en Running.

Escribe en /opt/diagnostico-13.txt una línea explicando la causa raíz.

Criterio de aceptación: el Deployment tiene 1/1 pods listos y el fichero de diagnóstico existe y no está vacío.


Tarea 14 — Resolución de problemas: el servicio que no responde

Namespace: rutas-norte-pre · Peso: 8 puntos · Objetivo: 8 min

El Service redis-cache-svc de rutas-norte-pre no está sirviendo tráfico: las aplicaciones que lo llaman reciben un rechazo de conexión, aunque los pods de redis-cache están Running. Encuentra la causa y arréglala sin modificar los pods.

Escribe en /opt/diagnostico-14.txt una línea explicando la causa raíz.

Criterio de aceptación: kubectl get endpoints redis-cache-svc -n rutas-norte-pre muestra al menos una dirección IP.


Tarea 15 — Resolución de problemas: el pod que nunca se planifica

Namespace: rutas-norte-pro · Peso: 8 puntos · Objetivo: 8 min

El Pod informes-ocupacion de rutas-norte-pro lleva minutos en estado Pending. Averigua por qué y consíguelo poner en Running. Puedes elegir entre corregir el pod o adaptar el clúster; justifica tu decisión.

Escribe en /opt/diagnostico-15.txt una línea explicando la causa raíz y la solución elegida.

Criterio de aceptación: el pod informes-ocupacion está en Running.


  1. Reparto del tiempo y estrategia

4.1 Tiempos objetivo

Tarea Tema Puntos Objetivo Acumulado
1 Deployment y escalado 5 4 min 4
2 Service e Ingress 7 8 min 12
3 ConfigMap y Secret 6 6 min 18
4 PV, PVC y Pod 8 9 min 27
5 Taints y nodeSelector 6 6 min 33
6 CronJob 5 5 min 38
7 Sondas y QoS 7 7 min 45
8 RBAC 7 7 min 52
9 NetworkPolicy 7 8 min 60
10 Pod Security 7 7 min 67
11 HPA 5 4 min 71
12 Rollout y rollback 6 6 min 77
13 TS: worker caído 8 8 min 85
14 TS: Service sin endpoints 8 8 min 93
15 TS: pod Pending 8 8 min 101
Revisión final ~19 min 120

Total: 100 puntos en 101 minutos de trabajo, con 19 de margen. Ese margen es deliberado: en el examen real se consume en dudas, tareas que se alargan y la revisión.

4.2 Estrategia sugerida

Aplicando lo aprendido en la lección 12-04:

Min 0-4     Bloque de arranque + lectura de las 15 tareas
Min 4-30    Fase 1 (baratas y rápidas): T1, T11, T6, T3, T12   → 27 puntos
Min 30-85   Fase 2 (bloque central por peso): T4, T13, T14, T15, T9, T8, T10, T7, T2, T5
Min 85-105  Fase 3: pendientes y medio hechas
Min 105-120 Revisión: contexto, namespace y funcionamiento de cada objeto

Si te ciñes al orden numérico también funciona, pero fíjate en que las tareas 11 y 6 valen 10 puntos entre las dos y se resuelven en 9 minutos: son las de mejor rendimiento del simulacro.

4.3 Recordatorio antes de empezar

  • Fija el namespace al empezar cada tarea, o usa -n en todos los comandos.
  • Verifica cada tarea antes de pasar a la siguiente.
  • Si una tarea supera su objetivo en más del 50 %, márcala y salta.
  • Nunca dejes una tarea completamente vacía: hay puntuación parcial.

Cuando termines, no leas todavía el solucionario. Primero haz los ejercicios integradores de la sección siguiente si te quedan fuerzas, o descansa y vuelve más tarde.


Errores Comunes y Consejos

Estos son los fallos que más se repiten en este simulacro concreto.

Error Tarea afectada Cómo evitarlo
Crear el Deployment sin la etiqueta pedida en los pods (solo en el Deployment) 1 La etiqueta va en spec.template.metadata.labels
Ingress con pathType: Exact en vez de Prefix 2 Usar --rule="host/*=svc:80" con asterisco
Confundir envFrom con env 3 "Todas las claves" → envFrom; "una clave" → valueFrom
storageClassName distinto entre PV y PVC 4 Debe coincidir exactamente, o el PVC queda Pending
Poner solo la tolerancia y olvidar el nodeSelector 5 La tolerancia permite, no obliga: hacen falta las dos cosas
backoffLimit en el nivel equivocado del CronJob 6 Va en jobTemplate.spec, no en CronJob.spec
QoS Burstable en vez de Guaranteed 7 requests y limits idénticos en CPU y memoria
Olvidar el subrecurso pods/log 8 Sin él, kubectl logs falla aunque puedas listar pods
NetworkPolicy sin policyTypes 9 Sin policyTypes: [Ingress] no hay denegación efectiva
Aplicar restricted y olvidar seccompProfile 10 El estándar restricted lo exige explícitamente
HPA con <unknown> en las métricas 11 Falta el metrics-server o el Deployment no tiene requests de CPU
rollout undo sin comprobar la imagen resultante 12 Verificar con jsonpath la imagen final
Diagnosticar sin mirar los eventos 13, 14, 15 kubectl describe y kubectl get events --sort-by=.lastTimestamp
Borrar pods de un Deployment para "arreglarlos" 13 Se recrean idénticos: hay que corregir la causa

Tres consejos finales para el simulacro:

  1. Trata las tres tareas de resolución de problemas como el examen real las trata: valen 24 de los 100 puntos, casi una cuarta parte. Si vas justo de tiempo, prioriza estas antes que las de creación.
  2. Si un objeto queda Pending o CrashLoopBackOff, la tarea no está hecha. No basta con que el YAML se haya aplicado.
  3. Anota el tiempo real de cada tarea. El análisis posterior de dónde se te fue el tiempo vale tanto como la nota.

Ejercicios

Estos tres retos integradores son de mayor calado que las tareas del simulacro: cada uno junta varios módulos del curso en un único objetivo. Hazlos después del simulacro, sin límite estricto de tiempo pero midiéndolo.

Ejercicio 1 — Componente completo desde cero

Partiendo de un namespace vacío rutas-norte-qa, deja operativo el componente api-reservas completo, listo para producción:

  1. Un ConfigMap config-api y un Secret credenciales-postgres.
  2. Un Deployment api-reservas con 3 réplicas, imagen nginx:1.27-alpine, que consuma el ConfigMap por variables y el Secret por fichero.
  3. Sondas de readiness y liveness, y QoS Guaranteed.
  4. Una ServiceAccount propia sa-api con permiso para leer únicamente el ConfigMap config-api, y sin montaje automático del token.
  5. Un Service ClusterIP y un Ingress con host qa.rutasnorte.es.
  6. Un HPA entre 3 y 9 réplicas al 70 % de CPU.
  7. Un PodDisruptionBudget que garantice al menos 2 pods disponibles.
  8. NetworkPolicy de denegación de entrada por defecto, con excepción para el namespace ingress-nginx.

Objetivo: 30 minutos. Criterio: todo Running, endpoints poblados, auth can-i correcto y el pod sin token montado.

Ejercicio 2 — Recuperar un servicio caído

Provoca deliberadamente esta situación en rutas-norte-pro y después recupérala como si fuera una incidencia real (usa el runbook mental del módulo 11):

El síntoma que te dan: "los usuarios reciben 503 al entrar en la tienda; el Ingress responde pero no llega nada detrás".

Preparación de la avería (que la haga otra persona, o hazla tú y espera un día para olvidar los detalles):

kubectl scale deployment tienda-web --replicas=0 -n rutas-norte-pro
kubectl patch svc tienda-web-svc -n rutas-norte-pro \
  -p '{"spec":{"ports":[{"port":80,"targetPort":8080}]}}'
kubectl set image deployment/tienda-web tienda-web=nginx:9.9-noexiste -n rutas-norte-pro

Lo que se te pide: diagnosticar en orden (Ingress → Service → endpoints → pods → contenedor), documentar cada hallazgo, arreglar las tres causas y dejar el servicio sirviendo con 3 réplicas. Escribe un breve informe de incidencia con línea temporal, causa raíz y acción preventiva.

Ejercicio 3 — Migración con cero cortes

En rutas-norte-pro, el Deployment tienda-web sirve tráfico. Migra su almacenamiento de configuración de un ConfigMap montado como fichero a un ConfigMap distinto con contenido nuevo, sin que ningún pod deje de estar disponible en ningún momento y sin que se sirva contenido inconsistente.

Pasos que debes resolver: crear el nuevo ConfigMap, ajustar la estrategia del Deployment, pausar el rollout para agrupar cambios, aplicar la modificación, reanudar, verificar en cada fase que hay al menos 3 pods listos, y tener preparado el rollout undo si algo falla.

Demuestra con un bucle de peticiones que no ha habido ni un solo fallo durante la migración.


Soluciones

Solución al Ejercicio 1

k create namespace rutas-norte-qa
k config set-context --current --namespace=rutas-norte-qa

# 1. Configuración
k create configmap config-api --from-literal=nivel_log=info --from-literal=max_conexiones=100
k create secret generic credenciales-postgres \
  --from-literal=usuario=reservas --from-literal=password='N0rt3-2026'

# 4a. ServiceAccount y RBAC mínimo
k create serviceaccount sa-api
k patch serviceaccount sa-api -p '{"automountServiceAccountToken": false}'
k create role lector-config --verb=get --resource=configmaps --resource-name=config-api
k create rolebinding lector-config-b --role=lector-config \
  --serviceaccount=rutas-norte-qa:sa-api

El Deployment con todo integrado:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reservas
  namespace: rutas-norte-qa
spec:
  replicas: 3
  selector:
    matchLabels: { app: api-reservas }
  template:
    metadata:
      labels: { app: api-reservas }
    spec:
      serviceAccountName: sa-api
      automountServiceAccountToken: false
      containers:
      - name: api
        image: nginx:1.27-alpine
        ports:
        - containerPort: 80
        envFrom:
        - configMapRef: { name: config-api }
        volumeMounts:
        - name: secretos
          mountPath: /etc/secretos
          readOnly: true
        resources:
          requests: { cpu: 200m, memory: 256Mi }
          limits:   { cpu: 200m, memory: 256Mi }
        readinessProbe:
          httpGet: { path: /, port: 80 }
          initialDelaySeconds: 3
          periodSeconds: 10
        livenessProbe:
          httpGet: { path: /, port: 80 }
          periodSeconds: 15
          failureThreshold: 3
      volumes:
      - name: secretos
        secret: { secretName: credenciales-postgres }
k apply -f api-qa.yaml

# 5. Exposición
k expose deployment api-reservas --name=api-reservas-svc --port=80 --target-port=80
k create ingress api-ingress --class=nginx --rule="qa.rutasnorte.es/*=api-reservas-svc:80"

# 6. HPA
k autoscale deployment api-reservas --min=3 --max=9 --cpu-percent=70

# 7. PDB
k create poddisruptionbudget api-pdb --selector=app=api-reservas --min-available=2
# 8. NetworkPolicies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: denegar-entrada, namespace: rutas-norte-qa }
spec:
  podSelector: {}
  policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: permitir-desde-ingress, namespace: rutas-norte-qa }
spec:
  podSelector:
    matchLabels: { app: api-reservas }
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
    ports:
    - protocol: TCP
      port: 80

Verificación completa:

k get deploy,svc,ing,hpa,pdb,netpol -n rutas-norte-qa
k describe pod -l app=api-reservas -n rutas-norte-qa | grep "QoS Class"     # Guaranteed
k exec deploy/api-reservas -n rutas-norte-qa -- ls /var/run/secrets/kubernetes.io/serviceaccount
# No such file or directory  ← el token NO está montado
k auth can-i get configmap/config-api --as=system:serviceaccount:rutas-norte-qa:sa-api -n rutas-norte-qa   # yes
k auth can-i list configmaps --as=system:serviceaccount:rutas-norte-qa:sa-api -n rutas-norte-qa            # no

Las dos trampas del ejercicio: automountServiceAccountToken: false hay que ponerlo en el pod (o en la SA), y el --resource-name=config-api en el Role es lo que convierte "leer un ConfigMap concreto" en un permiso realmente mínimo.

Solución al Ejercicio 2

Diagnóstico en el orden correcto, de fuera hacia dentro:

# 1. El Ingress: ¿tiene backend?
k describe ingress tienda-ingress -n rutas-norte-pro | grep -A5 Rules
Host                Path  Backends
www.rutasnorte.es   /     tienda-web-svc:80 (<error: endpoints "tienda-web-svc" not found>)
# 2. El Service: ¿tiene endpoints?
k get endpoints tienda-web-svc -n rutas-norte-pro
NAME             ENDPOINTS   AGE
tienda-web-svc   <none>      42m
# 3. ¿Hay pods?
k get pods -l app=tienda-web -n rutas-norte-pro
k get deploy tienda-web -n rutas-norte-pro
No resources found.
NAME         READY   UP-TO-DATE   AVAILABLE
tienda-web   0/0     0            0

Hallazgo 1: el Deployment está escalado a 0.

k scale deployment tienda-web --replicas=3 -n rutas-norte-pro
k get pods -l app=tienda-web -n rutas-norte-pro
NAME                          READY   STATUS             RESTARTS
tienda-web-7d9f8c6b4d-abc12   0/1     ImagePullBackOff   0

Hallazgo 2: imagen inexistente.

k describe pod -l app=tienda-web -n rutas-norte-pro | grep -A3 Events
# Failed to pull image "nginx:9.9-noexiste": manifest unknown

k set image deployment/tienda-web tienda-web=nginx:1.27-alpine -n rutas-norte-pro
k rollout status deployment/tienda-web -n rutas-norte-pro
k get endpoints tienda-web-svc -n rutas-norte-pro
NAME             ENDPOINTS
tienda-web-svc   <none>

Los pods están Running pero el Service sigue sin endpoints: hay una tercera causa.

k get svc tienda-web-svc -n rutas-norte-pro -o yaml | grep -A4 ports
  ports:
  - port: 80
    targetPort: 8080     ← los pods escuchan en el 80

Hallazgo 3: targetPort equivocado.

k patch svc tienda-web-svc -n rutas-norte-pro \
  -p '{"spec":{"ports":[{"port":80,"targetPort":80,"protocol":"TCP"}]}}'
k get endpoints tienda-web-svc -n rutas-norte-pro
NAME             ENDPOINTS
tienda-web-svc   10.244.1.7:80,10.244.2.5:80,10.244.2.6:80

Informe de incidencia:

INCIDENCIA: 503 en www.rutasnorte.es
Impacto: tienda-web no disponible, 42 minutos.
Línea temporal:
  T+0  Alerta de 503 desde el Ingress
  T+2  Ingress OK, Service sin endpoints
  T+4  Deployment escalado a 0 -> escalado a 3
  T+6  Pods en ImagePullBackOff (nginx:9.9-noexiste) -> imagen corregida
  T+9  Pods Running pero Service aún sin endpoints
  T+11 targetPort 8080 frente a puerto real 80 -> corregido
  T+12 Servicio restablecido
Causa raíz: tres cambios manuales no revisados sobre produccion.
Prevención: gestionar rutas-norte-pro por GitOps (modulo 10), impedir cambios
manuales con RBAC, y alerta sobre "Service sin endpoints" en Prometheus.

La lección de este ejercicio: un síntoma puede esconder varias causas encadenadas. Arreglar la primera y dar por cerrada la incidencia es el error clásico. La verificación de extremo a extremo (endpoints poblados) es lo único que demuestra que está resuelto.

Solución al Ejercicio 3

# 0. Bucle de comprobación en otro terminal
while true; do
  k run probe-$RANDOM --rm -q --image=busybox:1.36 --restart=Never -n rutas-norte-pro \
    -- wget -qO- --timeout=2 tienda-web-svc >/dev/null 2>&1 \
    && echo -n "." || echo -n "X"
  sleep 1
done
# 1. Nuevo ConfigMap
k create configmap config-web-v2 \
  --from-literal=index.html='<h1>Rutas Norte v2</h1>' -n rutas-norte-pro

# 2. Estrategia sin cortes
k patch deployment tienda-web -n rutas-norte-pro -p '
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0'

# 3. Pausar para agrupar cambios
k rollout pause deployment/tienda-web -n rutas-norte-pro

# 4. Aplicar los cambios (ninguno se despliega todavía)
k patch deployment tienda-web -n rutas-norte-pro --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/volumes/0/configMap/name","value":"config-web-v2"}
]'
k annotate deployment tienda-web -n rutas-norte-pro \
  kubernetes.io/change-cause="migracion a config-web-v2" --overwrite

# 5. Reanudar: ahora sí, un único rollout
k rollout resume deployment/tienda-web -n rutas-norte-pro
k rollout status deployment/tienda-web -n rutas-norte-pro --timeout=120s

Verificación durante el proceso (en un tercer terminal):

watch -n1 'kubectl get deploy tienda-web -n rutas-norte-pro; \
           kubectl get endpoints tienda-web-svc -n rutas-norte-pro'
NAME         READY   UP-TO-DATE   AVAILABLE
tienda-web   4/3     1            3          ← nunca baja de 3 disponibles

El bucle de peticiones debe mostrar solo puntos, sin ninguna X.

Plan de reversión preparado antes de empezar:

k rollout undo deployment/tienda-web -n rutas-norte-pro
k rollout status deployment/tienda-web -n rutas-norte-pro

Las tres claves del ejercicio:

  1. maxUnavailable: 0 es lo que garantiza que nunca falten pods; maxSurge: 1 es lo que permite que el rollout avance (con ambos a 0 se bloquearía).
  2. rollout pause / resume evita dos rollouts consecutivos cuando hay varios cambios: sin pausar, el patch del volumen y la anotación provocarían dos oleadas de reemplazo.
  3. La readinessProbe es imprescindible: sin ella, Kubernetes considera "disponible" a un pod que todavía no sirve, y sí habría cortes aunque maxUnavailable sea 0.

Solucionario del Simulacro

Ahora sí. Corrige tarea por tarea y anota tu puntuación.

Tarea 1 (5 puntos)

k create deployment tienda-web --image=nginx:1.27-alpine --replicas=4 \
  -n rutas-norte-dev $do > t1.yaml

Añadir la etiqueta en la plantilla del pod:

  template:
    metadata:
      labels:
        app: tienda-web
        entorno: dev          # ← aquí
k apply -f t1.yaml
k scale deployment tienda-web --replicas=6 -n rutas-norte-dev

Comprobación:

k get deploy tienda-web -n rutas-norte-dev
k get pods -n rutas-norte-dev --show-labels | head -3
NAME         READY   UP-TO-DATE   AVAILABLE
tienda-web   6/6     6            6

tienda-web-6c9d...   1/1  Running  app=tienda-web,entorno=dev,pod-template-hash=...

La trampa: poner entorno=dev solo en metadata.labels del Deployment. Los pods no la heredan: va en spec.template.metadata.labels. Alternativa válida y más rápida: crear el Deployment y después k label pods -l app=tienda-web entorno=dev, pero entonces los pods nuevos del escalado no la llevarían.

Tarea 2 (7 puntos)

k expose deployment tienda-web --name=tienda-web-svc \
  --port=80 --target-port=80 -n rutas-norte-dev

k create ingress tienda-ingress -n rutas-norte-dev --class=nginx \
  --rule="www.rutasnorte.es/*=tienda-web-svc:80"

Comprobación:

k get endpoints tienda-web-svc -n rutas-norte-dev
k describe ingress tienda-ingress -n rutas-norte-dev | grep -A4 Rules
NAME             ENDPOINTS
tienda-web-svc   10.244.1.5:80,10.244.1.6:80,... (6 direcciones)

Rules:
  Host                Path  Backends
  www.rutasnorte.es   /     tienda-web-svc:80 (10.244.1.5:80,...)

La trampa: el /* genera pathType: Prefix. Sin el asterisco obtendrías Exact, que solo casa la raíz literal. Y si el describe muestra <error: endpoints not found>, el Service está mal.

Tarea 3 (6 puntos)

k create configmap config-api --from-literal=nivel_log=info \
  --from-literal=max_conexiones=100 -n rutas-norte-dev

k create secret generic credenciales-postgres \
  --from-literal=usuario=reservas --from-literal=password='N0rt3-2026' -n rutas-norte-dev

k run api-reservas --image=nginx:1.27-alpine -n rutas-norte-dev $do > t3.yaml
spec:
  containers:
  - name: api-reservas
    image: nginx:1.27-alpine
    envFrom:
    - configMapRef:
        name: config-api
    volumeMounts:
    - name: secretos
      mountPath: /etc/secretos
      readOnly: true
  volumes:
  - name: secretos
    secret:
      secretName: credenciales-postgres

Comprobación:

k exec api-reservas -n rutas-norte-dev -- env | grep -E 'nivel_log|max_conexiones'
k exec api-reservas -n rutas-norte-dev -- cat /etc/secretos/usuario
nivel_log=info
max_conexiones=100
reservas

La trampa: envFrom es hermano de env, no hijo. Y "montar el Secret como ficheros" significa volumen, no secretKeyRef.

Tarea 4 (8 puntos)

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-reservas
spec:
  capacity: { storage: 3Gi }
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath: { path: /mnt/datos-reservas }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-reservas
  namespace: rutas-norte-pre
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: manual
  resources:
    requests: { storage: 2Gi }
---
apiVersion: v1
kind: Pod
metadata:
  name: postgres-reservas
  namespace: rutas-norte-pre
spec:
  containers:
  - name: postgres
    image: busybox:1.36
    command: ["sleep", "3600"]
    volumeMounts:
    - name: datos
      mountPath: /var/lib/datos
  volumes:
  - name: datos
    persistentVolumeClaim:
      claimName: pvc-reservas

Comprobación:

k get pv pv-reservas
k get pvc,pod -n rutas-norte-pre
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
pv-reservas   3Gi        RWO            Retain           Bound    rutas-norte-pre/pvc-reservas

NAME                                 STATUS   VOLUME        CAPACITY
persistentvolumeclaim/pvc-reservas   Bound    pv-reservas   3Gi

La trampa doble: el PV es de ámbito de clúster (sin namespace), y el storageClassName: manual debe estar en ambos. Si lo omites en el PVC, el aprovisionador dinámico por defecto intentaría crear otro volumen y el PVC no se enlazaría al tuyo.

Tarea 5 (6 puntos)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-pro
spec:
  replicas: 2
  selector:
    matchLabels: { app: worker-notificaciones }
  template:
    metadata:
      labels: { app: worker-notificaciones }
    spec:
      nodeSelector:
        tipo: batch
      tolerations:
      - key: dedicado
        operator: Equal
        value: batch
        effect: NoSchedule
      containers:
      - name: worker
        image: busybox:1.36
        command: ["sleep", "3600"]

Comprobación:

k get pods -l app=worker-notificaciones -n rutas-norte-pro -o wide
NAME                      READY   STATUS    NODE
worker-notificaciones-x   1/1     Running   simulacro-worker2
worker-notificaciones-y   1/1     Running   simulacro-worker2

La trampa: hacen falta las dos piezas. La toleration solo permite que el pod entre en ese nodo; el nodeSelector es lo que lo obliga a ir ahí. Con solo la tolerancia, los pods irían a cualquier nodo libre.

Tarea 6 (5 puntos)

apiVersion: batch/v1
kind: CronJob
metadata:
  name: informes-nocturnos
  namespace: rutas-norte-pro
spec:
  schedule: "30 2 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: informes
            image: busybox:1.36
            command: ["/bin/sh", "-c", "echo informe de ocupacion generado"]

Comprobación:

k get cronjob informes-nocturnos -n rutas-norte-pro
k create job prueba-t6 --from=cronjob/informes-nocturnos -n rutas-norte-pro
k logs job/prueba-t6 -n rutas-norte-pro
informe de ocupacion generado

La trampa: backoffLimit va en jobTemplate.spec; concurrencyPolicy y successfulJobsHistoryLimit en CronJob.spec. Y restartPolicy debe ser OnFailure o Never: Always hace que la API rechace el manifiesto.

Tarea 7 (7 puntos)

k edit deployment api-reservas -n rutas-norte-pre
      containers:
      - name: nginx
        image: nginx:1.27-alpine
        resources:
          requests: { cpu: 200m, memory: 256Mi }
          limits:   { cpu: 200m, memory: 256Mi }
        readinessProbe:
          httpGet: { path: /, port: 80 }
          initialDelaySeconds: 3
          periodSeconds: 10
        livenessProbe:
          httpGet: { path: /, port: 80 }
          periodSeconds: 10
          failureThreshold: 3

Comprobación:

k rollout status deployment/api-reservas -n rutas-norte-pre
k describe pod -l app=api-reservas -n rutas-norte-pre | grep "QoS Class"
QoS Class:  Guaranteed

La trampa: Guaranteed exige requests idénticos a limits en CPU y memoria y en todos los contenedores. Poner solo limits también funciona (Kubernetes copia los requests), pero poner requests distintos de limits da Burstable y la tarea puntúa a la mitad.

Tarea 8 (7 puntos)

k create serviceaccount soporte-n1 -n rutas-norte-pro

k create role soporte-lectura --verb=get,list,watch \
  --resource=pods,pods/log -n rutas-norte-pro

k create rolebinding soporte-lectura-b --role=soporte-lectura \
  --serviceaccount=rutas-norte-pro:soporte-n1 -n rutas-norte-pro

k set serviceaccount deployment/tienda-web soporte-n1 -n rutas-norte-pro

Comprobación:

SA=system:serviceaccount:rutas-norte-pro:soporte-n1
k auth can-i list pods --as=$SA -n rutas-norte-pro          # yes
k auth can-i get pods/log --as=$SA -n rutas-norte-pro       # yes
k auth can-i delete pods --as=$SA -n rutas-norte-pro        # no
k auth can-i list pods --as=$SA -n rutas-norte-dev          # no
k get deploy tienda-web -n rutas-norte-pro \
  -o jsonpath='{.spec.template.spec.serviceAccountName}'    # soporte-n1

La trampa: pods/log es un recurso aparte. Sin él, kubectl logs devuelve Forbidden aunque puedas listar pods. Y usar ClusterRole+ClusterRoleBinding daría acceso a todos los namespaces: violaría el criterio.

Tarea 9 (7 puntos)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-todo-entrada
  namespace: rutas-norte-pro
spec:
  podSelector: {}
  policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-tienda-desde-ingress
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: tienda-web
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80

Comprobación:

k get netpol -n rutas-norte-pro

# Desde dentro del namespace: debe FALLAR
k run test --rm -it --image=busybox:1.36 --restart=Never -n rutas-norte-pro \
  -- wget -qO- --timeout=3 tienda-web-svc
wget: download timed out

La trampa: sin policyTypes: [Ingress] en la primera política, el objeto existe pero no deniega nada. Y kubernetes.io/metadata.name es una etiqueta que Kubernetes pone automáticamente en todos los namespaces desde la 1.21: es la forma fiable de seleccionar uno por nombre.

Tarea 10 (7 puntos)

k label namespace rutas-norte-pre \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 --overwrite
apiVersion: v1
kind: Pod
metadata:
  name: auditor-seguro
  namespace: rutas-norte-pre
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: auditor
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]

Comprobación:

k get ns rutas-norte-pre --show-labels
k get pod auditor-seguro -n rutas-norte-pre
k run inseguro --image=nginx -n rutas-norte-pre
Error from server (Forbidden): pods "inseguro" is forbidden:
violates PodSecurity "restricted:v1.30": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile

La trampa: el estándar restricted exige cuatro cosas a la vez y la gente olvida seccompProfile: RuntimeDefault. El mensaje de error las enumera todas: úsalo como lista de comprobación.

Efecto lateral a tener en cuenta: al aplicar enforce en rutas-norte-pre, el Deployment api-reservas de la tarea 7 seguirá corriendo (PSA no expulsa pods existentes), pero no podrá crear pods nuevos. Si haces la tarea 10 antes que la 7 o la 11, el rollout se bloqueará. Es exactamente el tipo de interacción entre tareas que aparece en el examen real.

Tarea 11 (5 puntos)

k autoscale deployment api-reservas --min=2 --max=8 --cpu-percent=65 -n rutas-norte-pre

Comprobación:

k get hpa -n rutas-norte-pre
NAME           REFERENCE                 TARGETS        MINPODS   MAXPODS   REPLICAS
api-reservas   Deployment/api-reservas   cpu: 0%/65%    2         8         2

La trampa: si TARGETS muestra <unknown>/65%, hay dos causas posibles: el metrics-server no está instalado o no responde, o el Deployment no tiene requests de CPU (el HPA calcula el porcentaje sobre el request). Si hiciste la tarea 7 antes, los requests ya están y esto funciona; si no, hay que añadirlos.

Tarea 12 (6 puntos)

k patch deployment tienda-web -n rutas-norte-pro -p '
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0'

k set image deployment/tienda-web nginx=nginx:1.27.2-alpine -n rutas-norte-pro
k annotate deployment tienda-web -n rutas-norte-pro \
  kubernetes.io/change-cause="subida a 1.27.2" --overwrite

k rollout status deployment/tienda-web -n rutas-norte-pro
k rollout history deployment/tienda-web -n rutas-norte-pro
k rollout undo deployment/tienda-web -n rutas-norte-pro

Comprobación:

k rollout status deployment/tienda-web -n rutas-norte-pro
k get deploy tienda-web -n rutas-norte-pro \
  -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.27-alpine

La trampa: el nombre del contenedor en set image no es libre. Compruébalo primero:

k get deploy tienda-web -n rutas-norte-pro \
  -o jsonpath='{.spec.template.spec.containers[*].name}'

Si set image no encuentra el contenedor, el comando dice error: unable to find container named ... y no hace nada.

Tarea 13 (8 puntos) — Resolución de problemas

k get pods -n rutas-norte-dev
NAME                                    READY   STATUS                       RESTARTS
worker-notificaciones-5d8f...-abc       0/1     CreateContainerConfigError   0
k describe pod -l app=worker-notificaciones -n rutas-norte-dev | tail -8
Events:
  Warning  Failed  30s (x9 over 3m)  kubelet
    Error: secret "credenciales-cola" not found

Causa raíz: el Deployment referencia con envFrom un Secret que no existe en el namespace.

k create secret generic credenciales-cola \
  --from-literal=host=cola.rutasnorte.es --from-literal=token=abc123 \
  -n rutas-norte-dev

k get pods -n rutas-norte-dev -w
echo "El Deployment referenciaba con envFrom el Secret credenciales-cola, inexistente en rutas-norte-dev" \
  > /opt/diagnostico-13.txt

Comprobación:

k get deploy worker-notificaciones -n rutas-norte-dev
cat /opt/diagnostico-13.txt
NAME                    READY   UP-TO-DATE   AVAILABLE
worker-notificaciones   1/1     1            1

La trampa: CreateContainerConfigError siempre apunta a un ConfigMap o Secret ausente, o a una clave inexistente dentro de uno que sí existe. No lo confundas con CrashLoopBackOff (el proceso arranca y muere) ni con ImagePullBackOff (problema de imagen). Y el enunciado prohíbe cambiar la configuración del contenedor: la solución es crear el Secret, no quitar el envFrom.

Tarea 14 (8 puntos) — Resolución de problemas

k get pods,svc,endpoints -n rutas-norte-pre | grep redis
pod/redis-cache-6f8b...-xyz   1/1     Running
service/redis-cache-svc       ClusterIP   10.96.87.4   6379/TCP
endpoints/redis-cache-svc     <none>                    ← el síntoma

Los pods están bien pero el Service no los ve. Eso solo tiene una explicación: el selector no casa las etiquetas.

k get svc redis-cache-svc -n rutas-norte-pre -o jsonpath='{.spec.selector}'
k get pods -l app=redis-cache -n rutas-norte-pre --show-labels
{"app":"redis"}
NAME                       LABELS
redis-cache-6f8b...-xyz    app=redis-cache,pod-template-hash=6f8b...

Causa raíz: el Service selecciona app=redis, pero los pods llevan app=redis-cache.

k patch svc redis-cache-svc -n rutas-norte-pre \
  -p '{"spec":{"selector":{"app":"redis-cache"}}}'

echo "El selector del Service era app=redis y los pods llevan app=redis-cache" \
  > /opt/diagnostico-14.txt

Comprobación:

k get endpoints redis-cache-svc -n rutas-norte-pre
NAME              ENDPOINTS
redis-cache-svc   10.244.2.9:6379

La trampa: el enunciado prohíbe tocar los pods, así que cambiar sus etiquetas no vale (y además rompería el ReplicaSet, que los recrearía). Hay que corregir el Service. Recuerda la regla: Service sin endpoints = selector que no casa en el 90 % de los casos; el 10 % restante son pods no Ready por una readinessProbe fallando.

Tarea 15 (8 puntos) — Resolución de problemas

k get pod informes-ocupacion -n rutas-norte-pro
k describe pod informes-ocupacion -n rutas-norte-pro | tail -6
NAME                 READY   STATUS    RESTARTS   AGE
informes-ocupacion   0/1     Pending   0          14m

Events:
  Warning  FailedScheduling  2m (x8 over 14m)  default-scheduler
    0/3 nodes are available: 1 node(s) had untolerated taint
    {node-role.kubernetes.io/control-plane: }, 2 node(s) didn't match
    Pod's node affinity/selector.

Causa raíz: el pod tiene nodeSelector: disco=nvme y ningún nodo lleva esa etiqueta.

k get pod informes-ocupacion -n rutas-norte-pro -o jsonpath='{.spec.nodeSelector}'
k get nodes --show-labels | grep disco || echo "ningun nodo tiene la etiqueta disco"

Dos soluciones válidas. La mejor es etiquetar el nodo, porque el nodeSelector expresa un requisito real (un disco rápido para generar informes) y el pod es inmutable en ese campo:

k label node simulacro-worker disco=nvme
k get pod informes-ocupacion -n rutas-norte-pro -o wide
NAME                 READY   STATUS    NODE
informes-ocupacion   1/1     Running   simulacro-worker

La alternativa —recrear el pod sin nodeSelector— también deja el pod Running, pero pierde la intención del diseño:

k get pod informes-ocupacion -n rutas-norte-pro -o yaml > t15.yaml
# eliminar la sección nodeSelector
k delete pod informes-ocupacion -n rutas-norte-pro
k apply -f t15.yaml
echo "nodeSelector disco=nvme sin ningun nodo etiquetado; solucion: etiquetar simulacro-worker con disco=nvme para respetar el requisito de planificacion" \
  > /opt/diagnostico-15.txt

La trampa: el mensaje de FailedScheduling es la respuesta completa y hay que saber leerlo. didn't match Pod's node affinity/selector es nodeSelector o afinidad; untolerated taint es un taint sin tolerancia; Insufficient cpu/memory son recursos; pod has unbound immediate PersistentVolumeClaims es almacenamiento. Cuatro mensajes, cuatro causas distintas.


Autoevaluación

Suma los puntos obtenidos y busca tu franja.

Puntuación Veredicto Qué hacer
90-100 Listo para presentarte. Tu nivel está claramente por encima del umbral de aprobado, con margen para un mal día. Reserva la fecha ya. Dedica los días previos a mantener velocidad, no a estudiar temas nuevos.
75-89 Prácticamente listo. Aprobarías, pero sin colchón. Identifica las 2-3 tareas que fallaste, repasa sus lecciones y repite el simulacro en una semana. Reserva la fecha para dentro de 2-3 semanas.
60-74 En el límite. Es la franja del "puede caer de cualquier lado". Repasa los dominios de tus fallos con las tablas de mapeo de las lecciones 12-01, 12-02 o 12-03. Practica 2 semanas más y repite el simulacro.
40-59 Repasar dominios concretos. El conocimiento está, pero hay lagunas y falta velocidad. Vuelve a las lecciones de los módulos donde fallaste (ver tabla siguiente). Trabaja 3-4 semanas antes de repetir.
0-39 Volver a los módulos. Faltan fundamentos, no solo práctica. Rehaz el curso desde el módulo correspondiente, con el clúster delante y haciendo todos los ejercicios.

Diagnóstico por tarea fallada

Cada tarea apunta a un módulo concreto. Si fallaste una, sabes exactamente adónde volver.

Tarea fallada Dominio Vuelve a
1 Cargas de trabajo 02-03-deployments
2 Servicios y redes 04-02-tipos-de-servicios, 04-04-controladores-de-ingress
3 Configuración 03-01-configmaps, 03-02-secrets, 03-03-variables-de-entorno
4 Almacenamiento 05-02-volumenes-persistentes, 05-03-reclamaciones-de-volumenes-persistentes
5 Planificación 06-05-planificacion-afinidad-taints-y-tolerations
6 Cargas por lotes 06-03-trabajos-y-cronjobs
7 Observabilidad y recursos 07-01-verificaciones-de-salud-y-sondas, 03-05-limitranges-y-clases-de-qos
8 Seguridad y acceso 08-01-control-de-acceso-basado-en-roles, 03-06-serviceaccounts-y-acceso-a-la-api
9 Seguridad de red 04-06-politicas-de-red, 08-04-seguridad-de-red
10 Endurecimiento 08-02-contextos-de-seguridad-y-endurecimiento, 08-03-politicas-de-seguridad-de-pods
11 Escalado 09-01-autoescalado-horizontal-de-pods
12 Despliegues 02-04-actualizaciones-rollbacks-y-estrategias
13, 14, 15 Resolución de problemas 07-06-depuracion-y-eventos-del-cluster, 11-06-operacion-en-produccion

Análisis del tiempo

Además de la nota, revisa tus tiempos anotados:

Situación Qué significa Remedio
Nota alta pero sin acabar a tiempo Sabes hacerlo, pero despacio Alias, $do, copiar de la documentación (lección 12-04)
Acabaste pronto con nota baja Vas rápido pero con lagunas Repasar los módulos de la tabla anterior
Te atascaste más de 15 min en una tarea Falta disciplina de tiempo Entrenar el límite duro y el salto sin remordimientos
Fallaste por namespace o contexto Problema de proceso, no de conocimiento Entrenar el ciclo contexto → resolver → verificar
Fallaste las tres de troubleshooting El dominio de mayor peso del CKA Rompe tu clúster a propósito y arréglalo, una y otra vez

Conclusión

Aquí termina el curso.

Empezaste, en el módulo 1, lanzando un Pod suelto contra un clúster recién montado y preguntándote por qué demonios hacía falta tanta maquinaria para ejecutar un contenedor. Terminas resolviendo un examen de quince tareas contrarreloj sobre una plataforma completa en producción.

Entre esos dos puntos está el recorrido de Rutas Norte. Le diste forma con Deployments y Services, la configuraste sin meter contraseñas en el código, la publicaste con Ingress y TLS, le diste memoria con volúmenes persistentes y copias de seguridad, la enseñaste a arrancar en el orden correcto con initContainers y a sobrevivir al mantenimiento de un nodo con taints y PodDisruptionBudgets. Le pusiste ojos con Prometheus y Grafana, la cerraste con RBAC, NetworkPolicies, Pod Security y firma de imágenes, la hiciste crecer sola con HPA y KEDA, la empaquetaste con Helm y Kustomize, la desplegaste sola desde Git con Argo CD, y la operaste de verdad: con incidencias reales, runbooks, despliegues canario y una factura de costes que había que justificar.

Qué sabes hacer ahora, dicho sin adornos:

  • Diseñar y desplegar una aplicación completa en Kubernetes, desde el manifiesto hasta la URL pública con certificado.
  • Elegir la primitiva correcta para cada problema: Deployment o StatefulSet, Job o CronJob, initContainer o sidecar, readiness o liveness.
  • Gestionar configuración y secretos sin exponerlos, y controlar el consumo de recursos de todo lo que corre.
  • Montar, actualizar y reparar un clúster: nodos, plano de control, etcd, certificados.
  • Diagnosticar. Y esto es lo que más va a distinguirte: ante un síntoma —un 503, un pod Pending, un Service sin endpoints— sabes por dónde empezar, qué comando ejecutar y cómo llegar a la causa raíz en minutos.
  • Asegurar lo que despliegas, en las tres fases: construcción, despliegue y ejecución.
  • Escalar, medir y ajustar, con datos en vez de intuiciones.
  • Operar en producción como un profesional: con runbooks, con procedimientos de reversión probados y con conciencia del coste.

Y si te presentas a una certificación, sabes también cómo funciona el examen, qué dominios entran, dónde estudió cada objetivo y cómo se gestionan dos horas de terminal contrarreloj.

Kubernetes va a seguir cambiando. Aparecerán APIs nuevas, se retirarán otras, la Gateway API acabará desplazando a Ingress, y herramientas que hoy son estándar serán reemplazadas. Lo que no cambia es lo que de verdad has aprendido: el modelo declarativo, el bucle de reconciliación, la separación entre deseo y realidad, y el método para averiguar por qué la realidad no coincide con el deseo. Eso se traslada a cualquier versión y a cualquier herramienta que venga después.

Monta un clúster propio y no lo apagues. Rompe cosas a propósito. Despliega tus proyectos ahí aunque no haga falta. La única forma de que esto no se oxide es usarlo.

Buen viaje.

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