Ejecutar Jobs y CronJobs de Kubernetes

KubernetesBeginner
Practicar Ahora

Introducción

Los Deployments mantienen los servicios en ejecución de forma continua. El trabajo por lotes tiene una condición de éxito distinta: ejecutarse hasta finalizar, registrar el resultado y detenerse. Kubernetes modela el trabajo puntual mediante Jobs y las ejecuciones repetitivas programadas mediante CronJobs.

Ejecutarás un Job, inspeccionarás su Pod y sus registros, añadirás paralelismo controlado, programarás un CronJob y aprenderás cómo la suspensión y los límites del historial facilitan la gestión operativa de las tareas recurrentes.

Considera este laboratorio como una comparación de condiciones de éxito. Un Deployment tiene éxito al mantener disponibles sus réplicas. Un Job tiene éxito cuando el número requerido de Pods termina con el código de salida 0. Un CronJob tiene éxito al crear Jobs en los momentos previstos. Mantener separadas estas tres capas hará que el YAML resulte mucho más fácil de leer.

Ejecutar un Job puntual

En este paso crearás un Job cuyo Pod imprimirá un informe y finalizará correctamente. Crea el manifiesto con un documento aquí; el EOF entre comillas evita que el shell expanda los caracteres dentro del YAML.

Antes de introducir el YAML, observa su estructura: kind: Job selecciona el controlador, spec.template describe el Pod que creará y el Pod contiene un único contenedor de corta duración. A diferencia de una plantilla de Deployment, un Pod gestionado por un Job debe usar restartPolicy: Never u OnFailure, porque se espera que el trabajo finalice.

cd /home/labex/project/batch-workloads
cat <<'EOF' > report-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: report-job
  namespace: practice-batch
spec:
  backoffLimit: 2
  template:
    metadata:
      labels:
        app: report-job
    spec:
      restartPolicy: Never
      containers:
        - name: reporter
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo report-complete"]
EOF

Primero pide a Kubernetes que valide el archivo localmente. --dry-run=client comprueba la estructura del objeto sin crear nada, una práctica útil cada vez que escribes un manifiesto nuevo:

kubectl apply --dry-run=client -f report-job.yaml

Si el resultado es job.batch/report-job created (dry run), el archivo tiene una estructura válida. Aplícalo de verdad y espera a que el Job alcance la condición Complete, en lugar de intentar adivinar cuánto tardará:

kubectl apply -f report-job.yaml
kubectl wait --for=condition=Complete job/report-job -n practice-batch --timeout=60s

restartPolicy: Never permite que el controlador del Job decida si hace falta otro Pod. backoffLimit: 2 limita los reintentos tras los fallos. Lee la salida del Pod completado a través del controlador:

kubectl logs job/report-job -n practice-batch

Deberías ver report-complete. Al indicar job/report-job, kubectl localiza automáticamente el Pod del Job, por lo que no necesitas copiar el nombre generado.

Los tres comandos producen un ciclo de vida breve y fácil de leer:

job.batch/report-job created
job.batch/report-job condition met
report-complete

condition met significa que el controlador del Job ha registrado la finalización; la línea siguiente corresponde a la salida de la aplicación. En un clúster local recién iniciado, incluso este pequeño Job puede tardar varios segundos mientras se estabiliza la red. Esperar a una condición hace que el flujo sea fiable sin tener que elegir arbitrariamente una duración para sleep.

Rastrear la propiedad y la finalización de un Job

En este paso compararás un Job completado con un Deployment que se ejecuta continuamente. Un Job completado conserva su Pod para que los registros y el estado de terminación sigan disponibles para su inspección.

Empieza con la vista tabular habitual. El Job debería mostrar una finalización correcta, mientras que su Pod debería tener la fase Completed o Succeeded, según la columna que se esté mostrando:

kubectl get jobs,pods -n practice-batch
kubectl get pods -n practice-batch -l job-name=report-job -o custom-columns='POD:.metadata.name,PHASE:.status.phase,OWNER:.metadata.ownerReferences[0].name,EXIT_CODE:.status.containerStatuses[0].state.terminated.exitCode'
kubectl describe job report-job -n practice-batch

La fase del Pod debería ser Succeeded, su propietario debería ser report-job y el código de salida debería ser 0. Un Job se completa cuando el número requerido de Pods termina correctamente; no reinicia un contenedor exitoso simplemente para mantenerlo en ejecución.

La consulta de columnas personalizadas convierte cuatro campos anidados en una pequeña tabla de evidencias. Es más útil que leer todo el YAML del Pod cuando la pregunta concreta es: «¿quién es el propietario de este Pod y el proceso terminó correctamente?».

El resultado de la prueba fue el siguiente:

POD                PHASE       OWNER        EXIT_CODE
report-job-cd54c   Succeeded   report-job   0

El sufijo generado será diferente. Lee la fila de izquierda a derecha: OWNER=report-job vincula el Pod con su controlador, mientras que PHASE=Succeeded junto con EXIT_CODE=0 demuestra que el proceso finalizó correctamente.

Ejecutar varias finalizaciones en paralelo

En este paso crearás un Job que requiere cuatro Pods completados correctamente y permite ejecutar dos al mismo tiempo.

Antes de escribir el manifiesto, es importante distinguir dos valores independientes:

  • completions: 4 significa que se requieren cuatro unidades de trabajo completadas correctamente en total.
  • parallelism: 2 significa que Kubernetes puede ejecutar como máximo dos de esas unidades simultáneamente.

completionMode: Indexed asigna a las unidades de trabajo los índices 0 a 3. Cada Pod recibe su propio índice mediante la variable de entorno JOB_COMPLETION_INDEX, lo que permite que los programas reales de procesamiento por lotes dividan un conjunto de datos sin que todos los Pods procesen la misma partición.

cat <<'EOF' > indexed-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: indexed-job
  namespace: practice-batch
spec:
  completionMode: Indexed
  completions: 4
  parallelism: 2
  backoffLimitPerIndex: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: worker
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo worker-index=${JOB_COMPLETION_INDEX}; sleep 2"]
EOF

Aplica el manifiesto y espera a que finalicen los cuatro índices:

El último comando usa -L para añadir como columna adicional el valor de la etiqueta batch.kubernetes.io/job-completion-index. Así puedes ver a qué índice de trabajo corresponde cada Pod generado sin abrir su YAML completo.

kubectl apply -f indexed-job.yaml
kubectl wait --for=condition=Complete job/indexed-job -n practice-batch --timeout=90s
kubectl get pods -n practice-batch -l job-name=indexed-job -L batch.kubernetes.io/job-completion-index

completions indica el número total de unidades de trabajo completadas correctamente; parallelism indica el número máximo de Pods simultáneos. La finalización indexada proporciona a cada Pod un índice estable mediante JOB_COMPLETION_INDEX.

La tabla final debería contener cuatro Pods completados con etiquetas de índice diferentes. Es posible que no aparezcan en orden numérico, ya que la finalización es concurrente y, por tanto, no determinista.

Por ejemplo:

NAME                  STATUS      JOB-COMPLETION-INDEX
indexed-job-0-gw5wq   Completed   0
indexed-job-1-vw4qk   Completed   1
indexed-job-2-mpvnr   Completed   2
indexed-job-3-8z5j8   Completed   3

Ver cuatro filas no significa que los cuatro Pods se hayan ejecutado simultáneamente. Los cuatro índices demuestran la cantidad total de trabajo, mientras que parallelism: 2 limitó cuántos Pods podía ejecutar Kubernetes a la vez.

Programar un CronJob

En este paso crearás un CronJob que inicia un Job cada minuto. La sintaxis de cron tiene cinco campos: minuto, hora, día del mes, mes y día de la semana.

La programación */1 * * * * significa «cada minuto». Se ha elegido deliberadamente una frecuencia alta para que puedas observarla durante un laboratorio corto. En producción, las programaciones suelen ser menos frecuentes. El manifiesto también contiene una plantilla jobTemplate anidada, porque un CronJob crea Jobs y cada Job crea después sus Pods.

cat <<'EOF' > heartbeat-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: heartbeat
  namespace: practice-batch
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: heartbeat
              image: busybox:1.36
              imagePullPolicy: IfNotPresent
              command: ["sh", "-c", "date -u; echo heartbeat-complete"]
EOF

Aplica la programación e inspecciona su resumen:

kubectl apply -f heartbeat-cronjob.yaml
kubectl get cronjob heartbeat -n practice-batch

concurrencyPolicy: Forbid impide que una nueva ejecución se solape con otra anterior que aún no haya terminado. Crea inmediatamente un Job a partir de la plantilla del CronJob para inspeccionarlo sin esperar al siguiente minuto:

kubectl create job --from=cronjob/heartbeat heartbeat-manual -n practice-batch
kubectl wait --for=condition=Complete job/heartbeat-manual -n practice-batch --timeout=60s
kubectl logs job/heartbeat-manual -n practice-batch

El registro contiene una marca temporal UTC seguida de heartbeat-complete. El Job creado manualmente utiliza la misma plantilla, pero no modifica la programación futura del CronJob.

La salida habitual es:

job.batch/heartbeat-manual condition met
Thu Aug 13 07:48:29 UTC 2026
heartbeat-complete

Tu marca temporal será diferente. Observa que el contenedor utiliza UTC porque su comando es date -u; así se evita confundir la programación del clúster con la zona horaria local del terminal.

Suspender y reanudar el trabajo programado

En este paso pausarás el CronJob sin eliminar su definición. La suspensión resulta útil durante tareas de mantenimiento o al responder a incidentes.

kubectl patch modifica únicamente el campo indicado en la carga JSON. Aquí establece spec.suspend en true; el objeto CronJob, su programación y los Jobs que ya existan permanecen intactos.

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":true}}'
kubectl get cronjob heartbeat -n practice-batch -o custom-columns='NAME:.metadata.name,SCHEDULE:.spec.schedule,SUSPEND:.spec.suspend,LAST_SCHEDULE:.status.lastScheduleTime'

Los Jobs existentes continúan ejecutándose; la suspensión solo impide que se creen nuevos Jobs programados. Reanúdalo después de la inspección:

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":false}}'
kubectl get jobs -n practice-batch --sort-by=.metadata.creationTimestamp

Ahora puedes distinguir las tres capas: el CronJob define una programación, cada ejecución programada crea un Job y cada Job crea uno o varios Pods.

Como todo el ejercicio puede terminar antes del siguiente cambio de minuto, LAST_SCHEDULE todavía puede mostrar <none>. No es un error: heartbeat-manual se creó bajo demanda mediante --from, no mediante el planificador del CronJob. El valor final importante es SUSPEND=false, que vuelve a permitir las ejecuciones programadas futuras.

Resumen

Has utilizado Jobs para trabajos finitos, finalizaciones indexadas para tareas paralelas y CronJobs para programaciones recurrentes. Has inspeccionado la propiedad del controlador, los registros, el estado de finalización, la política de concurrencia, los límites del historial y la suspensión. Estas diferencias del ciclo de vida son fundamentales para elegir el controlador de cargas de trabajo adecuado en Kubernetes.