Si el CKA certifica a quien mantiene el clúster vivo, el CKAD certifica a quien construye encima de él. Es la certificación del perfil que escribe el manifiesto de api-reservas, decide qué sonda usar, consume un Secret sin exponerlo, ajusta los resources para que el pod no muera bajo carga y publica una versión nueva sin cortar el servicio. Todo eso lo has hecho ya en Rutas Norte; esta lección lo reordena en clave de examen.
El CKAD tiene una particularidad que lo hace engañosamente duro: el temario es más sencillo que el del CKA, pero el examen es más agobiante. Hay muchas tareas y muy poco tiempo. La competencia que se mide no es solo saber Kubernetes: es saber Kubernetes rápido. Por eso la mitad de esta lección está dedicada a la velocidad con kubectl, que es lo que separa a quien aprueba de quien se queda a tres tareas del final.
Aviso imprescindible. Precio, duración exacta, número de tareas, porcentaje de aprobado y reparto de dominios cambian con el tiempo. Lo que sigue es orientativo y corresponde al momento de escribir. Consulta siempre el temario oficial vigente en la web de la Linux Foundation / CNCF (
training.linuxfoundation.org,cncf.io/certification/ckad) antes de matricularte.
Contenido
- Qué certifica el CKAD y en qué se diferencia del CKA
- El formato del examen y por qué el tiempo es el enemigo
- Los dominios del temario y su peso orientativo
- Mapa completo: cada objetivo del CKAD y su lección en este curso
- Los temas donde suele fallar la gente
- La caja de herramientas de velocidad
- Tabla maestra: lo que te van a pedir → la forma más rápida de hacerlo
- Ocho tareas tipo CKAD resueltas contrarreloj
- Qué certifica el CKAD y en qué se diferencia del CKA
El CKAD acredita que sabes diseñar, construir, configurar y desplegar aplicaciones nativas de la nube en Kubernetes. No te pregunta cómo se restaura etcd ni cómo se une un nodo al clúster: te da un clúster funcionando y te pide que pongas aplicaciones a correr correctamente en él.
1.1 La frontera exacta entre CKA y CKAD
| Pregunta | ¿CKA? | ¿CKAD? |
|---|---|---|
| Restaurar etcd, actualizar el clúster con kubeadm, arreglar un kubelet parado | Sí | No |
| Crear un PersistentVolume | Sí | Rara vez (sí el PVC) |
| Crear un Deployment con sondas y recursos | Sí | Sí, con más profundidad |
| Elegir entre initContainer y sidecar | Marginal | Sí, central |
| Consumir un ConfigMap de las cuatro formas posibles | Sí | Sí, exhaustivamente |
securityContext a nivel de contenedor |
Marginal | Sí |
| RBAC del clúster | Sí, central | Solo lo básico de ServiceAccounts |
| NetworkPolicies e Ingress | Sí | Sí |
En una frase: el CKA administra la casa, el CKAD amuebla las habitaciones. La zona común es grande, pero el ángulo con el que se pregunta es distinto.
1.2 Tabla comparativa CKA / CKAD / CKS
| Criterio | CKA | CKAD | CKS |
|---|---|---|---|
| Perfil | Administrador / SRE / plataforma | Desarrollador / ingeniero de aplicaciones | Ingeniero de seguridad |
| Foco | Clúster: nodos, control plane, etcd, red, RBAC | Aplicaciones: manifiestos, config, ciclo de vida | Endurecimiento, detección y respuesta |
| Requisito previo | Ninguno | Ninguno | CKA vigente obligatorio |
| Duración orientativa | ~2 horas | ~2 horas | ~2 horas |
| Nº de tareas orientativo | 15-20 | 15-20 | 15-16 |
| Aprobado orientativo | ~66 % | ~66 % | ~67 % |
| Dificultad percibida | Media-alta (diagnóstico profundo) | Media, con presión de tiempo extrema | Alta (temario denso y herramientas externas) |
| Lo que más suspende | No saber diagnosticar el plano de control | No dar tiempo a acabar | Desconocer Falco, kube-bench, AppArmor |
| Documentación permitida | kubernetes.io | kubernetes.io | kubernetes.io + Falco, Trivy y AppArmor |
Orden recomendado según tu perfil:
- Desarrollador: CKAD → CKA → (CKS si te especializas en seguridad).
- Operaciones / SRE / plataforma: CKA → CKS → (CKAD para cerrar el círculo).
- Desde cero, sin experiencia: CKAD primero; es más accesible y prepara el terreno para el CKA.
1.3 A quién le sirve
Desarrolladores cuyo equipo despliega en Kubernetes y quieren dejar de depender del equipo de plataforma; ingenieros de DevOps que escriben manifiestos a diario; perfiles de QA o de release que gestionan despliegues, sondas y configuración.
- El formato del examen y por qué el tiempo es el enemigo
| Aspecto | Descripción orientativa |
|---|---|
| Tipo | 100 % práctico, terminal en el navegador, supervisado |
| Duración | En torno a dos horas |
| Número de tareas | Aproximadamente entre 15 y 20 |
| Aprobado | Aproximadamente un 66 % |
| Clústeres | Varios, con cambio de contexto por tarea |
| Puntuación | Por tarea, con puntuaciones parciales |
| Documentación | Solo kubernetes.io/docs, en una pestaña adicional |
| Intentos | Suele incluir un segundo intento gratuito |
| Validez | En torno a dos años |
2.1 La aritmética que hay que interiorizar
120 minutos / 17 tareas = 7,0 min por tarea
- 10 minutos de revisión final = 6,4 min por tarea
- ~30 s de leer el enunciado y cambiar contexto = 5,9 min por tareaSeis minutos para leer, entender, ejecutar y verificar. Escribir a mano en vim un manifiesto de Deployment cuesta 4-5 minutos. Ahí está todo el problema y toda la solución: quien genera el YAML con --dry-run y lo retoca tarda 90 segundos; quien lo teclea entero, no acaba el examen.
2.2 El cambio de contexto
Cada tarea empieza con su contexto y su namespace:
kubectl config use-context rutas-norte-dev
kubectl config set-context --current --namespace=rutas-norte-devResolver una tarea en el clúster equivocado vale cero. Y en el CKAD casi todas indican un namespace concreto. Elige una estrategia y sé consistente:
- A: fijar el namespace en el contexto al empezar cada tarea (más rápido, exige acordarse de cambiarlo).
- B: poner
-n <ns>en absolutamente todos los comandos (más lento, imposible de olvidar).
2.3 Puntuación parcial: tu red de seguridad
Una tarea típica encadena tres o cuatro requisitos ("crea un pod con dos contenedores que compartan un emptyDir, el primero escribe y el segundo lee"). Si haces el pod y el volumen pero fallas el segundo contenedor, ya puntúas. Un pod a medias vale más que un pod inexistente: nunca dejes una tarea vacía.
- Los dominios del temario y su peso orientativo
Reparto publicado en el momento de escribir; verifícalo en la web oficial.
| Dominio | Peso orientativo | De qué va |
|---|---|---|
| Diseño y construcción de aplicaciones | ~20 % | Patrones multicontenedor, Jobs/CronJobs, volúmenes de aplicación, imágenes |
| Despliegue de aplicaciones | ~20 % | Deployments, estrategias, rollouts y rollbacks, Helm y Kustomize básicos |
| Observabilidad y mantenimiento | ~15 % | Sondas, logs, depuración, versionado de API y deprecaciones |
| Entorno, configuración y seguridad de la aplicación | ~25 % | ConfigMaps, Secrets, securityContext, ServiceAccounts, resources, CRDs, cuotas |
| Servicios y redes | ~20 % | Services, NetworkPolicies, Ingress |
El dominio de mayor peso es entorno, configuración y seguridad. Es también el que más se subestima porque "parece fácil": no lo es cuando te piden la variante concreta que no practicaste.
- Mapa completo: cada objetivo del CKAD y su lección en este curso
4.1 Diseño y construcción de aplicaciones (~20 %)
| Objetivo oficial | Lección del curso |
|---|---|
| Definir, construir y modificar imágenes de contenedor | 08-05-seguridad-de-imagenes, 11-03-cicd-con-kubernetes |
| Elegir y usar patrones multicontenedor (sidecar, adapter, ambassador) | 06-04-init-containers-sidecars-y-patrones |
| Comprender Jobs y CronJobs | 06-03-trabajos-y-cronjobs |
| Usar volúmenes de aplicación (emptyDir, PVC) | 05-01-volumenes, 05-03-reclamaciones-de-volumenes-persistentes |
| Init containers | 06-04-init-containers-sidecars-y-patrones |
| Elegir el recurso: Deployment, Pod o StatefulSet | 02-01-pods, 02-03-deployments, 06-01-statefulsets |
4.2 Despliegue de aplicaciones (~20 %)
| Objetivo oficial | Lección del curso |
|---|---|
| Usar Deployments y realizar rolling updates | 02-03-deployments, 02-04-actualizaciones-rollbacks-y-estrategias |
| Rollbacks y control del historial de revisiones | 02-04-actualizaciones-rollbacks-y-estrategias |
| Estrategias de despliegue: blue-green y canary | 11-04-estrategias-blue-green-y-canary |
| Usar Helm para desplegar paquetes existentes | 10-03-helm |
| Usar Kustomize para variantes de configuración | 10-04-kustomize |
| Escalar aplicaciones | 02-03-deployments, 09-01-autoescalado-horizontal-de-pods |
4.3 Observabilidad y mantenimiento (~15 %)
| Objetivo oficial | Lección del curso |
|---|---|
| Comprender el versionado de la API y las deprecaciones | 01-06-objetos-manifiestos-yaml-y-modelo-declarativo |
| Implementar sondas de liveness, readiness y startup | 07-01-verificaciones-de-salud-y-sondas |
| Usar herramientas de monitorización integradas | 07-02-servidor-de-metricas-y-kubectl-top |
| Usar los logs de los contenedores | 07-05-registro-centralizado-con-efk, 07-06-depuracion-y-eventos-del-cluster |
| Depurar en Kubernetes | 07-06-depuracion-y-eventos-del-cluster |
4.4 Entorno, configuración y seguridad de la aplicación (~25 %)
| Objetivo oficial | Lección del curso |
|---|---|
| Descubrir y usar recursos que extienden Kubernetes (CRD, operadores) | 06-06-definiciones-de-recursos-personalizados, 06-07-operadores-y-el-patron-controlador |
| Autenticación, autorización y control de admisión | 08-01-control-de-acceso-basado-en-roles, 08-03-politicas-de-seguridad-de-pods |
| Definir requests, limits y cuotas | 03-04-cuotas-y-limites-de-recursos, 03-05-limitranges-y-clases-de-qos |
| Comprender ConfigMaps | 03-01-configmaps, 03-03-variables-de-entorno |
| Crear y consumir Secrets | 03-02-secrets |
| Comprender ServiceAccounts | 03-06-serviceaccounts-y-acceso-a-la-api |
| Contextos de seguridad de aplicación | 08-02-contextos-de-seguridad-y-endurecimiento |
4.5 Servicios y redes (~20 %)
| Objetivo oficial | Lección del curso |
|---|---|
| Comprensión básica de NetworkPolicies | 04-06-politicas-de-red |
| Proveer y diagnosticar acceso desde fuera del clúster | 04-02-tipos-de-servicios, 04-04-controladores-de-ingress |
| Usar Services para exponer aplicaciones | 04-02-tipos-de-servicios, 04-03-dns-interno-y-descubrimiento-de-servicios |
| Ingress y reglas de enrutado | 04-04-controladores-de-ingress, 04-05-tls-y-certificados-con-cert-manager |
- Los temas donde suele fallar la gente
Estos ocho bloques concentran la mayoría de los suspensos.
5.1 Multicontenedor e initContainers
Un initContainer corre y termina antes de que arranquen los contenedores principales; un sidecar corre en paralelo durante toda la vida del pod. Desde Kubernetes 1.29 existe el sidecar nativo: un initContainer con restartPolicy: Always, que arranca antes y se mantiene vivo.
apiVersion: v1
kind: Pod
metadata:
name: api-reservas-con-sidecar
namespace: rutas-norte-dev
spec:
initContainers:
- name: espera-postgres # initContainer clásico: bloquea hasta terminar
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres-reservas 5432; do sleep 2; done']
- name: recolector-logs # sidecar NATIVO (1.29+): no termina nunca
image: busybox:1.36
restartPolicy: Always
command: ['sh', '-c', 'tail -F /var/log/api/app.log']
volumeMounts:
- { name: logs, mountPath: /var/log/api }
containers:
- name: api
image: node:20-alpine
command: ['sh', '-c', 'while true; do date >> /var/log/api/app.log; sleep 5; done']
volumeMounts:
- { name: logs, mountPath: /var/log/api }
volumes:
- name: logs
emptyDir: {}Errores típicos: poner el sidecar en containers cuando piden que arranque antes; olvidar que los contenedores de un pod comparten red (se ven en localhost) pero no el sistema de ficheros, que exige un volumen compartido.
5.2 Jobs y CronJobs
| Campo | Significado | Dónde va |
|---|---|---|
completions |
Ejecuciones exitosas necesarias | Job.spec |
parallelism |
Pods simultáneos | Job.spec |
backoffLimit |
Reintentos antes de dar el Job por fallido | Job.spec |
activeDeadlineSeconds |
Tiempo máximo total | Job.spec |
ttlSecondsAfterFinished |
Borrado automático al terminar | Job.spec |
schedule |
Expresión cron | CronJob.spec |
concurrencyPolicy |
Allow / Forbid / Replace |
CronJob.spec |
startingDeadlineSeconds |
Margen para lanzar una ejecución retrasada | CronJob.spec |
suspend |
Pausar el CronJob | CronJob.spec |
successfulJobsHistoryLimit |
Jobs terminados que se conservan | CronJob.spec |
apiVersion: batch/v1
kind: CronJob
metadata: { name: informes-ocupacion, namespace: rutas-norte-pro }
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 1800
template:
spec:
restartPolicy: OnFailure # obligatorio: nunca "Always"
containers:
- name: informes
image: rutas-norte/informes:1.4Error clásico: dejar restartPolicy: Always en la plantilla de un Job; la API lo rechaza. Segundo error: confundir los niveles, poniendo backoffLimit en CronJob.spec en vez de en jobTemplate.spec.
5.3 Elegir bien la sonda
| Sonda | Qué pasa si falla | Cuándo usarla |
|---|---|---|
livenessProbe |
El contenedor se reinicia | Detectar procesos colgados |
readinessProbe |
El pod sale de los endpoints del Service | El proceso vive pero aún no puede atender |
startupProbe |
Reinicia y desactiva liveness y readiness hasta pasar | Arranques lentos (JVM, migraciones) |
startupProbe:
httpGet: { path: /healthz, port: 3000 }
failureThreshold: 30
periodSeconds: 5 # hasta 150 s de margen para arrancar
readinessProbe:
httpGet: { path: /ready, port: 3000 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /healthz, port: 3000 }
periodSeconds: 15
failureThreshold: 3Errores típicos: poner livenessProbe donde el enunciado dice "no debe recibir tráfico hasta que..." (eso es readiness); usar initialDelaySeconds gigantes en lugar de una startupProbe.
5.4 securityContext a nivel de contenedor
| Campo | Pod | Contenedor |
|---|---|---|
runAsUser, runAsGroup, runAsNonRoot |
Sí | Sí (gana el del contenedor) |
fsGroup |
Sí | No |
seccompProfile |
Sí | Sí |
allowPrivilegeEscalation |
No | Sí |
readOnlyRootFilesystem |
No | Sí |
capabilities |
No | Sí |
privileged |
No | Sí |
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 2000
containers:
- name: api
image: rutas-norte/api-reservas:2.3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]Error clásico: poner capabilities o readOnlyRootFilesystem a nivel de pod. La API rechaza el manifiesto y pierdes dos minutos buscando por qué.
5.5 ConfigMaps y Secrets: las cuatro formas
El enunciado te pedirá una forma concreta. Esta tabla hay que sabérsela.
| Forma | Sintaxis | Cuándo la piden |
|---|---|---|
| Variable individual | env[].valueFrom.configMapKeyRef |
"la variable X toma el valor de la clave Y" |
| Todas las claves como variables | envFrom[].configMapRef |
"todas las claves como variables de entorno" |
| Fichero en un volumen | volumes[].configMap + volumeMounts |
"montado en /etc/config" |
| Solo una clave como fichero | volumes[].configMap.items |
"solo la clave app.conf, en /etc/config/app.conf" |
env:
- name: NIVEL_LOG # 1. variable individual
valueFrom:
configMapKeyRef: { name: config-api, key: nivel_log }
- name: DB_PASSWORD # 1bis. desde un Secret
valueFrom:
secretKeyRef: { name: credenciales-postgres, key: password }
envFrom:
- configMapRef: { name: config-api } # 2. todas las claves
- secretRef: { name: credenciales-postgres }
volumeMounts:
- { name: config, mountPath: /etc/config, readOnly: true }
volumes:
- name: config
configMap:
name: config-api
items: # 4. solo una clave (sin items → forma 3)
- { key: app.conf, path: app.conf }Creación imperativa, que es la que ahorra tiempo:
k create configmap config-api --from-literal=nivel_log=info --from-literal=timeout=30
k create configmap config-fichero --from-file=app.conf
k create secret generic credenciales-postgres \
--from-literal=usuario=reservas --from-literal=password='S3cr3t0!'
k create secret docker-registry regcred --docker-server=registry.rutasnorte.es \
--docker-username=ci --docker-password=xxx
k create secret tls tienda-web-tls --cert=tls.crt --key=tls.key5.6 resources y su efecto real
| Concepto | Efecto |
|---|---|
requests |
Lo que usa el planificador para elegir nodo. Reserva garantizada. |
limits |
Techo. La CPU se estrangula; la memoria provoca OOMKilled. |
Solo limits |
Kubernetes copia limits en requests |
| Nada | Clase BestEffort: primero en morir bajo presión |
| Clase de QoS | Condición |
|---|---|
Guaranteed |
Todos los contenedores tienen requests iguales a limits, en CPU y memoria |
Burstable |
Hay requests pero no coinciden con limits (o faltan algunos) |
BestEffort |
Ni requests ni limits en ningún contenedor |
Pregunta típica: "haz que el pod tenga QoS Guaranteed". Respuesta: requests y limits idénticos de CPU y memoria en todos los contenedores.
5.7 Actualizaciones y reversión de Deployments
k set image deployment/api-reservas api=rutas-norte/api-reservas:2.4
k rollout status deployment/api-reservas
k rollout history deployment/api-reservas
k rollout history deployment/api-reservas --revision=3
k rollout undo deployment/api-reservas # a la revisión ANTERIOR
k rollout undo deployment/api-reservas --to-revision=2
# Pausar para agrupar varios cambios en un solo rollout
k rollout pause deployment/api-reservas
k set resources deployment/api-reservas -c=api --limits=memory=512Mi
k rollout resume deployment/api-reservasspec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # pods extra permitidos por encima de replicas
maxUnavailable: 0 # ninguno puede faltar → despliegue sin corte5.8 NetworkPolicies
El fallo número uno es no entender que una NetworkPolicy que selecciona un pod lo convierte en aislado para los policyTypes declarados: a partir de ahí solo pasa lo explícitamente permitido.
# Denegar todo en el namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: denegar-todo, namespace: rutas-norte-pro }
spec:
podSelector: {} # {} = todos los pods del namespace
policyTypes: [Ingress, Egress]
---
# Permitir a api-reservas hablar con postgres y resolver DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-egress, namespace: rutas-norte-pro }
spec:
podSelector:
matchLabels: { app: api-reservas }
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels: { app: postgres-reservas }
ports:
- { protocol: TCP, port: 5432 }
- to: # DNS: casi siempre hay que permitirlo
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- { protocol: UDP, port: 53 }Detalle que cuesta puntos: el guion lo cambia todo.
# A) UNA entrada con dos selectores (AND): pods app=api EN namespaces entorno=pro
- namespaceSelector: { matchLabels: { entorno: pro } }
podSelector: { matchLabels: { app: api } }
# B) DOS entradas (OR): cualquier pod de namespaces entorno=pro, MÁS
# los pods app=api del namespace de la política
- namespaceSelector: { matchLabels: { entorno: pro } }
- podSelector: { matchLabels: { app: api } }
- La caja de herramientas de velocidad
Esta es la sección que decide el aprobado. Todo lo que sigue se teclea en los primeros 60 segundos.
6.1 El bloque de arranque
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 kCon eso, k run api --image=nginx $do > api.yaml genera el esqueleto y k delete pod api $now borra al instante.
6.2 --dry-run=client -o yaml: el generador universal
# Pods
k run tienda-web --image=nginx:1.27-alpine $do > pod.yaml
k run debug --image=busybox:1.36 $do --command -- sleep 3600 > debug.yaml
k run api --image=node:20 --labels=app=api,tier=backend --port=3000 \
--env=NIVEL_LOG=debug $do > api.yaml
# Deployments y Services
k create deployment api-reservas --image=rutas-norte/api:2.3 --replicas=3 $do > deploy.yaml
k expose deployment api-reservas --port=80 --target-port=3000 --name=api-svc $do > svc.yaml
k create service clusterip api-svc --tcp=80:3000 $do > svc.yaml
k create service nodeport tienda-svc --tcp=80:80 --node-port=30080 $do > svc.yaml
# Configuración, trabajos, Ingress y RBAC
k create configmap config-api --from-literal=nivel=info $do > cm.yaml
k create secret generic cred --from-literal=pass=xxx $do > sec.yaml
k create job migracion --image=rutas-norte/migra:1.0 $do > job.yaml
k create cronjob informes --image=rutas-norte/informes:1.4 --schedule="0 3 * * *" $do \
-- /bin/sh -c "informes.sh" > cj.yaml
k create ingress tienda --rule="www.rutasnorte.es/*=tienda-web-svc:80" $do > ing.yaml
k create serviceaccount soporte $do > sa.yaml
k create role lector --verb=get,list --resource=pods $do > role.yaml
k create rolebinding lector-b --role=lector --serviceaccount=ns:soporte $do > rb.yaml
k create quota cuota-qa --hard=cpu=4,memory=8Gi,pods=20 $do > quota.yaml
k create poddisruptionbudget api-pdb --selector=app=api --min-available=2 $do > pdb.yamlLo que NO tiene generador imperativo y hay que copiar de la documentación: PersistentVolume, PersistentVolumeClaim, NetworkPolicy, StorageClass, securityContext completo, sondas con todos sus campos y cualquier CRD.
6.3 kubectl explain: la documentación sin salir del terminal
k explain pod.spec.containers.livenessProbe --recursive
k explain pod.spec.securityContext --recursive
k explain cronjob.spec --recursive
k explain networkpolicy | head -3 # recordar el apiVersion correcto
k api-resources | grep -i policyEs más rápido que abrir el navegador para dudas del tipo "¿era failureThreshold o failureCount?".
6.4 kubectl run y create imperativos: los casos que caen
# Pods de un solo uso, para comprobar conectividad o DNS
k run test --image=busybox:1.36 --rm -it --restart=Never -- wget -qO- api-svc:80
k run dns --image=busybox:1.36 --rm -it --restart=Never -- nslookup api-svc
# Modificar objetos existentes sin tocar YAML
k scale deployment api-reservas --replicas=5
k autoscale deployment api-reservas --min=2 --max=10 --cpu-percent=70
k set image deployment/api-reservas api=rutas-norte/api:2.4
k set resources deployment/api-reservas -c=api --limits=cpu=500m,memory=512Mi
k set serviceaccount deployment/api-reservas soporte
k set env deployment/api-reservas NIVEL_LOG=debug
k set env deployment/api-reservas --from=configmap/config-api
k label pod tienda-web entorno=pro --overwrite
k annotate deployment api-reservas kubernetes.io/change-cause="subida a 2.4"
k cp rutas-norte-pro/api-reservas-abc:/var/log/app.log ./app.log6.5 Filtros y salidas que ahorran minutos
k get events -n rutas-norte-pro --sort-by=.lastTimestamp # el más útil al depurar
k get pods -A --sort-by=.status.containerStatuses[0].restartCount
k get pods -o name
k get pod api-reservas-abc -o jsonpath='{.status.podIP}'
k get pods -o custom-columns=NOMBRE:.metadata.name,NODO:.spec.nodeName,ESTADO:.status.phase
k get pods --field-selector status.phase=Running
k get pods -l 'entorno in (pre,pro)'
k get pods -l '!app' # pods SIN la etiqueta app
k logs -l app=api-reservas --tail=50 --all-containers=true
k logs api-reservas-abc -c sidecar --previous # el contenedor anterior al crash6.6 Edición rápida con vim
cat <<'EOF' > ~/.vimrc
set number
set expandtab
set tabstop=2
set shiftwidth=2
set softtabstop=2
set autoindent
set paste
EOF| Ajuste | Por qué importa |
|---|---|
expandtab |
YAML prohíbe tabuladores; sin esto tu manifiesto no valida |
tabstop/shiftwidth/softtabstop a 2 |
La indentación estándar de los manifiestos |
autoindent |
Mantiene el nivel al pulsar Enter |
set paste |
Crítico: evita que el autoindentado destroce el YAML pegado de la documentación |
number |
Los errores de la API dan número de línea |
| Acción | Comando | Acción | Comando |
|---|---|---|---|
| Ir a la línea N | :N |
Copiar / pegar línea | yy / p |
| Buscar / siguiente | /texto / n |
Deshacer / rehacer | u / Ctrl+r |
| Borrar línea / N líneas | dd / Ndd |
Indentar bloque | V, seleccionar, > o < |
| Reemplazar en el fichero | :%s/viejo/nuevo/g |
Guardar / salir sin guardar | :wq / :q! |
6.7 kubectl patch para cambios puntuales
# Réplicas
k patch deployment api-reservas -p '{"spec":{"replicas":5}}'
# Imagen de un contenedor concreto (strategic merge)
k patch deployment api-reservas \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"api","image":"rutas-norte/api:2.4"}]}}}}'
# Tipo de un Service
k patch svc tienda-web-svc -p '{"spec":{"type":"NodePort"}}'
# JSON patch para reemplazar un campo exacto
k patch pv pv-informes --type='json' \
-p='[{"op":"replace","path":"/spec/persistentVolumeReclaimPolicy","value":"Retain"}]'
# YAML multilinea (cómodo para listas)
k patch deployment worker-notificaciones --type='strategic' -p '
spec:
template:
spec:
tolerations:
- { key: dedicado, operator: Equal, value: batch, effect: NoSchedule }'Cuando patch no basta, KUBE_EDITOR=vim k edit deployment api-reservas. Cuidado: hay campos inmutables (el selector de un Deployment, el spec de un Job creado). Si edit falla por eso, la vía es exportar, borrar y recrear:
k get job migracion -o yaml > job.yaml # editar, quitar status y metadatos volátiles
k delete job migracion && k apply -f job.yaml
- Tabla maestra: lo que te van a pedir → la forma más rápida de hacerlo
| Enunciado típico | Forma más rápida |
|---|---|
| "Crea un pod X con imagen Y" | k run X --image=Y |
"...que ejecute sleep 3600" |
k run X --image=Y --command -- sleep 3600 |
"...con la etiqueta app=z" |
k run X --image=Y --labels=app=z |
| "...que exponga el puerto 8080" | k run X --image=Y --port=8080 |
| "...y se elimine al terminar" | k run X --image=Y --rm -it --restart=Never -- <cmd> |
| "Crea un Deployment con N réplicas" | k create deployment X --image=Y --replicas=N |
| "Escálalo a M réplicas" | k scale deployment X --replicas=M |
| "Expónlo internamente en el puerto 80" | k expose deployment X --port=80 --target-port=8080 |
| "Expónlo con NodePort 30080" | k create service nodeport X --tcp=80:8080 --node-port=30080 |
| "Actualiza la imagen a la versión Z" | k set image deployment/X c=img:Z |
| "Deshaz el último despliegue" | k rollout undo deployment/X |
| "Vuelve a la revisión 2" | k rollout undo deployment/X --to-revision=2 |
| "Comprueba el estado del despliegue" | k rollout status deployment/X |
| "Autoescálalo entre 2 y 10 al 70 % de CPU" | k autoscale deployment X --min=2 --max=10 --cpu-percent=70 |
| "Crea un ConfigMap con estas claves" | k create configmap X --from-literal=a=1 --from-literal=b=2 |
| "Crea un ConfigMap desde un fichero" | k create configmap X --from-file=app.conf |
| "Crea un Secret con usuario y contraseña" | k create secret generic X --from-literal=user=u --from-literal=pass=p |
| "Crea un Secret TLS" | k create secret tls X --cert=a.crt --key=a.key |
| "Descodifica el valor del Secret" | k get secret X -o jsonpath='{.data.pass}' | base64 -d |
| "Inyecta todas las claves como variables" | k set env deployment/D --from=configmap/X |
| "Job de 5 ejecuciones, 2 en paralelo" | k create job X --image=Y $do y añadir completions: 5, parallelism: 2 |
| "CronJob cada 5 minutos" | k create cronjob X --image=Y --schedule="*/5 * * * *" -- <cmd> |
| "Suspende el CronJob" | k patch cronjob X -p '{"spec":{"suspend":true}}' |
| "Lanza el CronJob ahora" | k create job manual --from=cronjob/X |
| "Ingress para el host H y ruta /r" | k create ingress X --class=nginx --rule="H/r*=svc:80" |
| "Crea una SA y úsala en el Deployment" | k create sa X + k set serviceaccount deployment/D X |
| "Crea un Role y su RoleBinding" | k create role R --verb=get,list --resource=pods + k create rolebinding B --role=R --serviceaccount=ns:sa |
| "Comprueba que tiene permiso" | k auth can-i list pods --as=system:serviceaccount:ns:sa -n ns |
| "Crea una cuota de recursos" | k create quota X --hard=cpu=4,memory=8Gi,pods=20 |
| "Añade tolerancia o nodeSelector" | Generar con $do y editar, o k patch |
| "Etiqueta el nodo" | k label node nodo-1 disco=ssd |
| "Encuentra el pod que más se reinicia" | k get pods -A --sort-by=.status.containerStatuses[0].restartCount |
| "Guarda los logs en un fichero" | k logs X -c c > /opt/salida.log |
| "Logs del contenedor que crasheó" | k logs X --previous |
| "Ordena los pods por CPU" | k top pods -n ns --sort-by=cpu |
| "Entra en el contenedor" | k exec -it X -c c -- sh |
| "Cuenta los pods con la etiqueta" | k get pods -l app=x --no-headers | wc -l |
| "Escribe el nombre del pod en un fichero" | k get pods -l app=x -o jsonpath='{.items[0].metadata.name}' > /opt/r.txt |
| "Elimina el pod inmediatamente" | k delete pod X $now |
- Ocho tareas tipo CKAD resueltas contrarreloj
Escenarios de Rutas Norte, cronómetro en marcha, solo kubernetes.io abierto.
Tarea 1 — Pod multicontenedor con volumen compartido (~6 %, objetivo: 6 min)
rutas-norte-dev. Crea un Podinformes-ocupacioncon dos contenedores que compartan unemptyDirllamadodatosmontado en/datos. El contenedorgenerador(busybox:1.36) escribe la fecha en/datos/ocupacion.logcada 5 segundos; el contenedorlector(misma imagen) muestra ese fichero por su salida estándar.
apiVersion: v1
kind: Pod
metadata: { name: informes-ocupacion, namespace: rutas-norte-dev }
spec:
containers:
- name: generador
image: busybox:1.36
command: ['sh', '-c', 'while true; do date >> /datos/ocupacion.log; sleep 5; done']
volumeMounts: [{ name: datos, mountPath: /datos }]
- name: lector
image: busybox:1.36
command: ['sh', '-c', 'tail -F /datos/ocupacion.log']
volumeMounts: [{ name: datos, mountPath: /datos }]
volumes:
- name: datos
emptyDir: {}La trampa: tail -F (mayúscula) espera a que el fichero exista; tail -f falla si el generador aún no lo ha creado. Y el volumen hay que montarlo en los dos contenedores.
Tarea 2 — ConfigMap y Secret consumidos de dos formas (~6 %, objetivo: 5 min)
rutas-norte-dev. Crea el ConfigMapconfig-apiconnivel_log=debugymax_conexiones=50, y el Secretcredenciales-postgresconusuario=reservasypassword=Nort3!2026. Crea un Podapi-reservas(nginx:1.27-alpine) que reciba todas las claves del ConfigMap como variables de entorno y monte el Secret como ficheros en/etc/secretos.
k create configmap config-api --from-literal=nivel_log=debug \
--from-literal=max_conexiones=50 -n rutas-norte-dev
k create secret generic credenciales-postgres --from-literal=usuario=reservas \
--from-literal=password='Nort3!2026' -n rutas-norte-dev
k run api-reservas --image=nginx:1.27-alpine $do -n rutas-norte-dev > api.yamlspec:
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 }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/usuarioLa trampa: envFrom es hermano de env, no hijo. Y el password lleva !: hay que entrecomillar con comillas simples para que bash no lo interprete.
Tarea 3 — CronJob con política de concurrencia (~6 %, objetivo: 5 min)
rutas-norte-pro. Crea un CronJobinformes-nocturnosa las 3:00 cada día, imagenbusybox:1.36, comandoecho informe generado. Sin ejecuciones solapadas, máximo 2 reintentos, y solo 3 ejecuciones exitosas en el historial.
k create cronjob informes-nocturnos --image=busybox:1.36 --schedule="0 3 * * *" \
-n rutas-norte-pro $do -- /bin/sh -c "echo informe generado" > cj.yamlspec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: informes-nocturnos
image: busybox:1.36
command: ["/bin/sh", "-c", "echo informe generado"]k create job prueba --from=cronjob/informes-nocturnos -n rutas-norte-pro
k logs job/prueba -n rutas-norte-proLa trampa: los niveles. backoffLimit en jobTemplate.spec; concurrencyPolicy y successfulJobsHistoryLimit en CronJob.spec. Y lanzar el Job con --from=cronjob/... es la forma de verificar sin esperar a las 3 de la mañana.
Tarea 4 — Sondas bien elegidas (~7 %, objetivo: 6 min)
rutas-norte-pre. El Deploymentapi-reservastarda hasta 90 segundos en arrancar. Configúralo para que (a) no reciba tráfico hasta responderGET /readyen el 3000; (b) se reinicie siGET /healthzfalla 3 veces seguidas; (c) esas dos sondas no actúen durante el arranque.
startupProbe:
httpGet: { path: /healthz, port: 3000 }
periodSeconds: 10
failureThreshold: 12 # 12 x 10s = 120 s de margen
readinessProbe:
httpGet: { path: /ready, port: 3000 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /healthz, port: 3000 }
periodSeconds: 10
failureThreshold: 3k rollout status deployment/api-reservas -n rutas-norte-pre
k describe pod -l app=api-reservas -n rutas-norte-pre | grep -E 'Liveness|Readiness|Startup'La trampa: la clave es el punto (c). La respuesta es startupProbe, que suspende liveness y readiness hasta pasar. Resolverlo con initialDelaySeconds: 90 no cumple el enunciado con precisión.
Tarea 5 — Rolling update sin corte y rollback (~7 %, objetivo: 7 min)
rutas-norte-pro. El Deploymenttienda-webdebe actualizarse sin que ningún pod deje de estar disponible. Configura la estrategia, actualiza la imagen anginx:1.27.2-alpineanotando la causa, y después revierte a la revisión anterior.
k patch deployment tienda-web -n rutas-norte-pro -p '
spec:
strategy:
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }'
k set image deployment/tienda-web tienda-web=nginx:1.27.2-alpine -n rutas-norte-pro
k annotate deployment tienda-web -n rutas-norte-pro \
kubernetes.io/change-cause="actualizacion a nginx 1.27.2" --overwrite
k rollout status deployment/tienda-web -n rutas-norte-pro
k rollout history deployment/tienda-web -n rutas-norte-prok rollout undo deployment/tienda-web -n rutas-norte-pro
k get deployment tienda-web -n rutas-norte-pro \
-o jsonpath='{.spec.template.spec.containers[0].image}'La trampa: el nombre del contenedor en set image no tiene por qué coincidir con el del Deployment. Compruébalo antes con -o jsonpath='{.spec.template.spec.containers[*].name}'.
Tarea 6 — securityContext restrictivo (~6 %, objetivo: 5 min)
rutas-norte-pro. Crea un Podworker-notificaciones(busybox:1.36,sleep 3600) que corra como usuario 10001 y grupo 3000, sin escalada de privilegios, con la raíz en solo lectura y descartando todas las capacidades del kernel.
apiVersion: v1
kind: Pod
metadata: { name: worker-notificaciones, namespace: rutas-norte-pro }
spec:
securityContext:
runAsUser: 10001
runAsGroup: 3000
runAsNonRoot: true
containers:
- name: worker
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }k exec worker-notificaciones -n rutas-norte-pro -- id
k exec worker-notificaciones -n rutas-norte-pro -- touch /pruebaLa trampa: runAsUser puede ir en el pod o en el contenedor; allowPrivilegeEscalation, readOnlyRootFilesystem y capabilities solo en el contenedor.
Tarea 7 — Service, Ingress y verificación (~7 %, objetivo: 7 min)
rutas-norte-pro. El Deploymenttienda-webescucha en el puerto 80. Expónlo con un Service ClusterIPtienda-web-svcy crea un Ingresstienda-ingressque enrutewww.rutasnorte.es/a ese Service, con la IngressClassnginx.
k expose deployment tienda-web --name=tienda-web-svc --port=80 --target-port=80 \
-n rutas-norte-pro
k create ingress tienda-ingress -n rutas-norte-pro --class=nginx \
--rule="www.rutasnorte.es/*=tienda-web-svc:80"
k describe ingress tienda-ingress -n rutas-norte-pro | grep -A4 Rules
k get endpoints tienda-web-svc -n rutas-norte-proRules:
Host Path Backends
---- ---- --------
www.rutasnorte.es / tienda-web-svc:80 (10.244.1.9:80,10.244.2.4:80)La trampa: el /* genera pathType: Prefix; sin el asterisco obtendrías Exact, que solo casa la raíz literal. Si los backends salen vacíos, el Service no está seleccionando pods.
Tarea 8 — Diagnóstico de un pod que no arranca (~7 %, objetivo: 6 min)
rutas-norte-dev. El podredis-cachelleva minutos sin pasar aRunning. Encuentra la causa, corrígela y deja constancia del motivo en/opt/diagnostico.txt.
k create configmap config-redis --from-literal=maxmemory=256mb -n rutas-norte-dev
k get pods -n rutas-norte-dev
echo "El pod referenciaba el ConfigMap config-redis, inexistente en el namespace" \
> /opt/diagnostico.txtLa trampa: distinguir los estados. Esta tabla resuelve el dominio de depuración del CKAD casi entero:
| Estado | Causa habitual |
|---|---|
ImagePullBackOff / ErrImagePull |
Imagen inexistente, tag mal escrito o credenciales de registro |
CreateContainerConfigError |
ConfigMap/Secret ausente o clave inexistente |
CrashLoopBackOff |
El proceso arranca y muere: mirar k logs --previous |
Pending |
Ningún nodo cumple requests/afinidad/taints, o PVC sin enlazar |
ContainerCreating prolongado |
Volumen que no monta o CNI con problemas |
OOMKilled en lastState |
El límite de memoria es demasiado bajo |
Errores Comunes y Consejos
| Error | Consecuencia | Prevención |
|---|---|---|
| Escribir el YAML a mano desde cero | Se te acaba el tiempo | $do + editar |
Pegar YAML en vim sin set paste |
Indentación en escalera, manifiesto inválido | ~/.vimrc en el minuto uno |
| Olvidar el namespace de la tarea | El objeto no puntúa | set-context --current --namespace= |
| No cambiar de contexto | Cero puntos | Primer comando, siempre |
| Confundir liveness con readiness | Media tarea perdida | "tráfico" → readiness; "reiniciar" → liveness |
restartPolicy: Always en un Job |
La API lo rechaza | OnFailure o Never |
capabilities a nivel de pod |
Manifiesto inválido | Solo a nivel de contenedor |
backoffLimit en CronJob.spec |
Campo ignorado o rechazado | Va en jobTemplate.spec |
| Atascarse 15 minutos en una tarea | Pierdes 3 tareas fáciles | Límite de 8 min y saltar |
| No verificar lo creado | Crees que puntúas y no | Un get/describe/exec de cierre |
apply sobre campos inmutables |
Error críptico | Exportar, borrar y recrear |
Consejos que marcan la diferencia:
- Cronometra siempre. Practicar sin reloj no prepara para el CKAD.
- Memoriza el bloque de arranque. Son 20 segundos que te devuelven 20 minutos.
- Lee el enunciado buscando el sustantivo clave: "que no reciba tráfico" → readiness; "todas las claves" →
envFrom; "solo esta clave" →items. - Practica copiar y pegar en el terminal del navegador: los atajos son distintos.
- Ten localizadas las páginas sin generador imperativo: PV/PVC, NetworkPolicy, securityContext y sondas.
- Haz siempre la parte que sabes. La puntuación parcial es real.
- Los últimos 10 minutos son para revisar, no para intentar una tarea nueva.
Ejercicios
Ejercicio 1 — Cinco tareas en quince minutos
En un clúster de práctica con el namespace rutas-norte-dev, resuelve en 15 minutos cronometrados:
- Deployment
tienda-webconnginx:1.27-alpine, 4 réplicas, expuesto como NodePort 30080. - ConfigMap
config-webconentorno=devycache=on, inyectado como variables en ese Deployment. - Un Job
migracion-bdconbusybox:1.36yecho migrado, 3 completions y 2 en paralelo. - Un HPA para
tienda-webentre 2 y 8 réplicas al 70 % de CPU. - Escribir en
/opt/pods.txtel nombre de todos los pods del namespace ordenados por nombre.
Ejercicio 2 — La tarea multicontenedor completa
Crea en rutas-norte-pre un Pod api-reservas-full que reúna todo lo difícil del CKAD, en 10 minutos:
- Un initContainer
espera-dbque bloquee hasta que respondapostgres-reservas:5432. - Un contenedor
apiconnginx:1.27-alpine, sondas de readiness y liveness sobre/puerto 80,requestsde 100m/128Mi ylimitsde 200m/256Mi. - Un sidecar nativo
logs(initContainer conrestartPolicy: Always) que hagatail -Fde un fichero compartido. - Un
emptyDircompartido entreapiylogsmontado en/var/log/nginx. securityContextconrunAsNonRoot,allowPrivilegeEscalation: falseydrop: ["ALL"].
Ejercicio 3 — Diagnóstico ciego
Pide a otra persona (o a un script) que rompa tres cosas en rutas-norte-dev sin decirte cuáles: un Deployment con imagen inexistente, un pod que referencia un Secret que no existe, un Service con el selector equivocado, un pod con limits.memory demasiado bajo, o un pod Pending por un nodeSelector imposible.
Encuentra y arregla las tres en 12 minutos, dejando en /opt/diagnostico.txt el síntoma y la causa de cada una.
Soluciones
Solución al Ejercicio 1
# 1 (≈90 s)
k create deployment tienda-web --image=nginx:1.27-alpine --replicas=4
k create service nodeport tienda-web --tcp=80:80 --node-port=30080
# 2 (≈60 s)
k create configmap config-web --from-literal=entorno=dev --from-literal=cache=on
k set env deployment/tienda-web --from=configmap/config-web
# 3 (≈150 s): generar y añadir completions/parallelism
k create job migracion-bd --image=busybox:1.36 $do -- /bin/sh -c "echo migrado" > job.yamlspec:
completions: 3
parallelism: 2
template:
spec:
restartPolicy: Never
containers:
- name: migracion-bd
image: busybox:1.36
command: ["/bin/sh", "-c", "echo migrado"]k apply -f job.yaml
# 4 (≈30 s)
k autoscale deployment tienda-web --min=2 --max=8 --cpu-percent=70
# 5 (≈30 s)
k get pods --sort-by=.metadata.name -o name > /opt/pods.txtEl truco del punto 2 es k set env --from=configmap/..., que inserta el envFrom sin tocar el YAML: escribirlo a mano habría costado dos minutos más. Y ojo con el punto 1: create service nodeport tienda-web hereda el selector app=tienda-web porque comparte nombre con el Deployment; verifícalo con k get endpoints tienda-web antes de dar la tarea por buena.
Solución al Ejercicio 2
apiVersion: v1
kind: Pod
metadata: { name: api-reservas-full, namespace: rutas-norte-pre }
spec:
securityContext: { runAsNonRoot: true, runAsUser: 10001 }
initContainers:
- name: espera-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres-reservas 5432; do sleep 2; done']
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
- name: logs
image: busybox:1.36
restartPolicy: Always # sidecar nativo (1.29+)
command: ['sh', '-c', 'tail -F /var/log/nginx/access.log']
volumeMounts: [{ name: logs-nginx, mountPath: /var/log/nginx }]
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
containers:
- name: api
image: nginx:1.27-alpine
ports: [{ containerPort: 80 }]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 200m, memory: 256Mi }
readinessProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 5
livenessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 15
failureThreshold: 3
volumeMounts: [{ name: logs-nginx, mountPath: /var/log/nginx }]
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumes:
- name: logs-nginx
emptyDir: {}Aviso práctico: nginx:1.27-alpine con runAsNonRoot y usuario 10001 no arranca tal cual, porque necesita escribir en /var/cache/nginx y /var/run. En el examen esto no importa (se corrige el manifiesto, no la ejecución), pero en la vida real habría que montar emptyDir en esos directorios o usar nginxinc/nginx-unprivileged. Es exactamente el detalle que viste en 08-02.
Solución al Ejercicio 3
La metodología es lo que se practica aquí:
k get pods -n rutas-norte-dev -o wide # 1. qué no está Running
k describe pod <nombre> -n rutas-norte-dev | tail -20 # 2. los eventos: 80 % de los casos
k logs <nombre> -n rutas-norte-dev --previous # 3. si arranca y muere
k get endpoints -n rutas-norte-dev # 4. si el problema es de Service
k get pods --show-labels -n rutas-norte-dev # ¿casan con el selector?
k get events -n rutas-norte-dev --sort-by=.lastTimestamp | tail -20| Síntoma observado | Causa | Arreglo |
|---|---|---|
ErrImagePull, evento manifest unknown |
Tag inexistente | k set image deployment/X c=imagen:tag-valido |
CreateContainerConfigError |
Secret/ConfigMap ausente | Crearlo con k create secret/configmap |
| Service sin endpoints | El selector no casa las etiquetas | k patch svc X -p '{"spec":{"selector":{"app":"correcto"}}}' |
OOMKilled en lastState.terminated.reason |
limits.memory bajo |
k set resources deployment/X -c=c --limits=memory=256Mi |
Pending con didn't match node selector |
nodeSelector imposible |
Etiquetar el nodo o quitar el selector |
cat <<'EOF' > /opt/diagnostico.txt
1. tienda-web: ErrImagePull por tag nginx:9.9 inexistente -> corregido a 1.27-alpine
2. api-reservas: CreateContainerConfigError, faltaba el Secret credenciales-postgres -> creado
3. redis-cache-svc: sin endpoints, selector app=redis frente a etiqueta app=redis-cache -> corregido
EOFError clásico de este ejercicio: borrar los pods rotos para "reiniciarlos". Si pertenecen a un Deployment, se recrean idénticos: hay que corregir la causa en la plantilla o en el objeto que falta.
Conclusión
El CKAD no premia al que más sabe de Kubernetes: premia al que resuelve más tareas correctas por minuto. Su temario es una cara conocida del curso —Deployments, ConfigMaps, Secrets, sondas, Jobs, Services, NetworkPolicies, securityContext— pero con un cronómetro encima que convierte la fluidez con kubectl en la competencia decisiva.
Lo esencial de esta lección:
- El CKAD certifica construir aplicaciones sobre el clúster, no administrarlo. La frontera con el CKA está en etcd, nodos y plano de control, que aquí no entran.
- El dominio de mayor peso es entorno, configuración y seguridad (~25 %): ConfigMaps, Secrets, recursos, QoS y
securityContext. - La aritmética manda: unos 6 minutos por tarea. Escribir YAML a mano es incompatible con aprobar.
- La caja de herramientas es innegociable:
alias k,$do, autocompletado,.vimrcconset paste,kubectl explain --recursiveykubectl patch. - Los ocho temas del apartado 5 —multicontenedor, Jobs, sondas,
securityContext, las cuatro formas de consumir configuración,resources, rollouts y NetworkPolicies— concentran la mayoría de los fallos. - La tabla del apartado 7 es tu chuleta de entrenamiento: enunciado típico → comando más rápido.
- Consulta siempre el temario oficial vigente en la web de la Linux Foundation / CNCF antes de matricularte.
Con el CKA cubriendo la administración y el CKAD el desarrollo, queda el tercer vértice: la seguridad. En la próxima lección abordamos el CKS, la certificación más exigente de las tres, la única que exige tener el CKA vigente para presentarse, y la que te pedirá manejar con soltura herramientas que ya viste en el módulo 8: kube-bench, Trivy, Falco, AppArmor, seccomp, Pod Security Admission y las políticas de admisión.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
