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

  1. Qué certifica el CKAD y en qué se diferencia del CKA
  2. El formato del examen y por qué el tiempo es el enemigo
  3. Los dominios del temario y su peso orientativo
  4. Mapa completo: cada objetivo del CKAD y su lección en este curso
  5. Los temas donde suele fallar la gente
  6. La caja de herramientas de velocidad
  7. Tabla maestra: lo que te van a pedir → la forma más rápida de hacerlo
  8. Ocho tareas tipo CKAD resueltas contrarreloj

  1. 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 No
Crear un PersistentVolume Rara vez (sí el PVC)
Crear un Deployment con sondas y recursos Sí, con más profundidad
Elegir entre initContainer y sidecar Marginal Sí, central
Consumir un ConfigMap de las cuatro formas posibles Sí, exhaustivamente
securityContext a nivel de contenedor Marginal
RBAC del clúster Sí, central Solo lo básico de ServiceAccounts
NetworkPolicies e Ingress

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.


  1. 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 tarea

Seis 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-dev

Resolver 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.


  1. 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.


  1. 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

  1. 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.4

Error 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: 3

Errores 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í (gana el del contenedor)
fsGroup No
seccompProfile
allowPrivilegeEscalation No
readOnlyRootFilesystem No
capabilities No
privileged No
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.key

5.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-reservas
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # pods extra permitidos por encima de replicas
      maxUnavailable: 0    # ninguno puede faltar → despliegue sin corte

5.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 } }

  1. 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 k

Con 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.yaml

Lo 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 policy

Es 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.log

6.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 crash

6.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

  1. 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

  1. 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 Pod informes-ocupacion con dos contenedores que compartan un emptyDir llamado datos montado en /datos. El contenedor generador (busybox:1.36) escribe la fecha en /datos/ocupacion.log cada 5 segundos; el contenedor lector (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: {}
k logs informes-ocupacion -c lector -n rutas-norte-dev
Wed Aug  6 09:14:22 UTC 2026
Wed Aug  6 09:14:27 UTC 2026

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 ConfigMap config-api con nivel_log=debug y max_conexiones=50, y el Secret credenciales-postgres con usuario=reservas y password=Nort3!2026. Crea un Pod api-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.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 }
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=debug
max_conexiones=50
reservas

La 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 CronJob informes-nocturnos a las 3:00 cada día, imagen busybox:1.36, comando echo 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.yaml
spec:
  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-pro
informe generado

La 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 Deployment api-reservas tarda hasta 90 segundos en arrancar. Configúralo para que (a) no reciba tráfico hasta responder GET /ready en el 3000; (b) se reinicie si GET /healthz falla 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: 3
k 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 Deployment tienda-web debe actualizarse sin que ningún pod deje de estar disponible. Configura la estrategia, actualiza la imagen a nginx:1.27.2-alpine anotando 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-pro
REVISION  CHANGE-CAUSE
1         <none>
2         actualizacion a nginx 1.27.2
k 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}'
nginx:1.27-alpine

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 Pod worker-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 /prueba
uid=10001 gid=3000
touch: /prueba: Read-only file system

La 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 Deployment tienda-web escucha en el puerto 80. Expónlo con un Service ClusterIP tienda-web-svc y crea un Ingress tienda-ingress que enrute www.rutasnorte.es/ a ese Service, con la IngressClass nginx.

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-pro
Rules:
  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 pod redis-cache lleva minutos sin pasar a Running. Encuentra la causa, corrígela y deja constancia del motivo en /opt/diagnostico.txt.

k get pods -n rutas-norte-dev
NAME          READY   STATUS                       RESTARTS   AGE
redis-cache   0/1     CreateContainerConfigError   0          4m
k describe pod redis-cache -n rutas-norte-dev | tail -6
Events:
  Warning  Failed  2m (x12 over 4m)  kubelet
    Error: configmap "config-redis" not found
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.txt

La 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:

  1. Cronometra siempre. Practicar sin reloj no prepara para el CKAD.
  2. Memoriza el bloque de arranque. Son 20 segundos que te devuelven 20 minutos.
  3. Lee el enunciado buscando el sustantivo clave: "que no reciba tráfico" → readiness; "todas las claves" → envFrom; "solo esta clave" → items.
  4. Practica copiar y pegar en el terminal del navegador: los atajos son distintos.
  5. Ten localizadas las páginas sin generador imperativo: PV/PVC, NetworkPolicy, securityContext y sondas.
  6. Haz siempre la parte que sabes. La puntuación parcial es real.
  7. 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:

  1. Deployment tienda-web con nginx:1.27-alpine, 4 réplicas, expuesto como NodePort 30080.
  2. ConfigMap config-web con entorno=dev y cache=on, inyectado como variables en ese Deployment.
  3. Un Job migracion-bd con busybox:1.36 y echo migrado, 3 completions y 2 en paralelo.
  4. Un HPA para tienda-web entre 2 y 8 réplicas al 70 % de CPU.
  5. Escribir en /opt/pods.txt el 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-db que bloquee hasta que responda postgres-reservas:5432.
  • Un contenedor api con nginx:1.27-alpine, sondas de readiness y liveness sobre / puerto 80, requests de 100m/128Mi y limits de 200m/256Mi.
  • Un sidecar nativo logs (initContainer con restartPolicy: Always) que haga tail -F de un fichero compartido.
  • Un emptyDir compartido entre api y logs montado en /var/log/nginx.
  • securityContext con runAsNonRoot, allowPrivilegeEscalation: false y drop: ["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.yaml
spec:
  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.txt

El 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
EOF

Error 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, .vimrc con set paste, kubectl explain --recursive y kubectl 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

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