--- title: "Cómo configurar Jobs y CronJobs en Kubernetes" description: "Crea Jobs y CronJobs en Kubernetes para tareas por lotes y programadas: reintentos, paralelismo, zona horaria, concurrencia y una copia de PostgreSQL diaria." category: "Kubernetes Avanzado" language: es source: https://cubepath.com/es/docs/kubernetes-avanzado/configuracion-de-trabajo-y-cronjob-de-kubernetes updated: 2026-09-25 --- # Cómo configurar Jobs y CronJobs en Kubernetes Un Job de Kubernetes ejecuta uno o varios pods hasta que terminan con éxito, reintentándolos si fallan. Un CronJob crea Jobs de forma periódica según una expresión cron, igual que `crontab` pero dentro del clúster. En este tutorial crearás un Job sencillo, lo ajustarás con reintentos, límites de tiempo y paralelismo, y terminarás con un CronJob real que hace una copia diaria de una base de datos PostgreSQL. ## Requisitos previos Para seguir esta guía necesitas: - Un clúster de Kubernetes 1.30 o superior, por ejemplo sobre VPS de CubePath con Ubuntu 24.04. - `kubectl` configurado contra el clúster con permisos para crear recursos en un namespace. - Para el paso 6, una StorageClass que pueda crear volúmenes (compruébalo con `kubectl get storageclass`) y un servidor PostgreSQL accesible desde el clúster. Crea un namespace para las pruebas y hazlo el predeterminado de tu contexto, así no tendrás que añadir `-n` a cada comando: ```bash kubectl create namespace batch kubectl config set-context --current --namespace=batch ``` ## Paso 1: Crear tu primer Job Un Job contiene una plantilla de pod igual que un Deployment, con una diferencia importante: `restartPolicy` debe ser `Never` u `OnFailure`, porque el pod tiene que poder terminar. ```bash nano hello-job.yaml ``` ```yaml apiVersion: batch/v1 kind: Job metadata: name: hello spec: template: spec: containers: - name: hello image: busybox:1.36 command: ["sh", "-c", "echo 'Procesando...'; sleep 5; echo 'Hecho'"] restartPolicy: Never ``` Crea el Job y espera a que termine. `kubectl wait` sale en cuanto el Job alcanza la condición `Complete`: ```bash kubectl apply -f hello-job.yaml kubectl wait --for=condition=complete job/hello --timeout=60s ``` ```output job.batch/hello condition met ``` Revisa el estado y la salida del pod. Kubernetes etiqueta los pods de un Job con `job-name`, y `kubectl logs job/` los encuentra directamente: ```bash kubectl get job hello kubectl logs job/hello ``` ```output NAME STATUS COMPLETIONS DURATION AGE hello Complete 1/1 8s 15s Procesando... Hecho ``` El Job y su pod se quedan en el clúster después de terminar para que puedas leer los registros. Bórralo cuando ya no lo necesites: ```bash kubectl delete job hello ``` ## Paso 2: Controlar reintentos, duración y limpieza Los campos de `spec` de un Job definen qué pasa cuando algo va mal y cuánto tiempo se conserva: | Campo | Qué hace | Valor por defecto | |-------|----------|-------------------| | `backoffLimit` | Número de reintentos antes de marcar el Job como fallido | `6` | | `activeDeadlineSeconds` | Tiempo máximo total del Job; al superarlo se detienen sus pods | Sin límite | | `ttlSecondsAfterFinished` | Segundos tras terminar (con éxito o no) antes de borrar el Job y sus pods | Sin borrado | Los reintentos se hacen con una espera exponencial (10 s, 20 s, 40 s...) hasta un máximo de seis minutos. Para verlo, crea un Job que siempre falla: ```bash nano failing-job.yaml ``` ```yaml apiVersion: batch/v1 kind: Job metadata: name: failing spec: backoffLimit: 2 activeDeadlineSeconds: 300 ttlSecondsAfterFinished: 600 template: spec: containers: - name: task image: busybox:1.36 command: ["sh", "-c", "echo 'No encuentro el archivo de entrada'; exit 1"] restartPolicy: Never ``` ```bash kubectl apply -f failing-job.yaml kubectl wait --for=condition=failed job/failing --timeout=120s kubectl get pods -l job-name=failing ``` Verás tres pods en estado `Error`: el intento original y los dos reintentos: ```output NAME READY STATUS RESTARTS AGE failing-4ktzq 0/1 Error 0 52s failing-8w2mn 0/1 Error 0 41s failing-vx9rd 0/1 Error 0 20s ``` El motivo del fallo aparece en los eventos del Job: ```bash kubectl describe job failing | tail -n 5 ``` ```output Normal SuccessfulCreate 52s job-controller Created pod: failing-4ktzq Normal SuccessfulCreate 41s job-controller Created pod: failing-8w2mn Normal SuccessfulCreate 20s job-controller Created pod: failing-vx9rd Warning BackoffLimitExceeded 10s job-controller Job has reached the specified backoff limit ``` Gracias a `ttlSecondsAfterFinished: 600`, el Job y sus pods desaparecerán solos a los diez minutos. > Consejo: con `restartPolicy: OnFailure` el contenedor se reinicia dentro del mismo pod en lugar de crear uno nuevo. Ocupa menos recursos, pero pierdes los registros de los intentos anteriores. `Never` es más fácil de depurar. ## Paso 3: Ejecutar tareas en paralelo Dos campos controlan cuántos pods se ejecutan y cuántos deben terminar bien: - `completions`: número de pods que deben acabar con éxito para dar el Job por completado. - `parallelism`: número máximo de pods ejecutándose a la vez. Con `completionMode: Indexed`, cada pod recibe un índice único de `0` a `completions - 1` en la variable de entorno `JOB_COMPLETION_INDEX`. Es la forma más sencilla de repartir trabajo: cada pod procesa su trozo según su índice. ```bash nano indexed-job.yaml ``` ```yaml apiVersion: batch/v1 kind: Job metadata: name: process-chunks spec: completions: 6 parallelism: 3 completionMode: Indexed ttlSecondsAfterFinished: 3600 template: spec: containers: - name: worker image: busybox:1.36 command: ["sh", "-c", "echo \"Procesando el fragmento $JOB_COMPLETION_INDEX\"; sleep 10"] restartPolicy: Never ``` ```bash kubectl apply -f indexed-job.yaml kubectl get pods -l job-name=process-chunks --watch ``` Verás que nunca hay más de tres pods en `Running` al mismo tiempo. Pulsa `Ctrl+C` cuando todos estén en `Completed` y comprueba la salida de cada índice: ```bash kubectl logs -l job-name=process-chunks --prefix ``` ```output [pod/process-chunks-0-jx7fp/worker] Procesando el fragmento 0 [pod/process-chunks-1-b2kqd/worker] Procesando el fragmento 1 [pod/process-chunks-2-m8wzs/worker] Procesando el fragmento 2 [pod/process-chunks-3-r4tlc/worker] Procesando el fragmento 3 [pod/process-chunks-4-n6vhp/worker] Procesando el fragmento 4 [pod/process-chunks-5-q9dfx/worker] Procesando el fragmento 5 ``` ```bash kubectl get job process-chunks ``` ```output NAME STATUS COMPLETIONS DURATION AGE process-chunks Complete 6/6 24s 40s ``` ## Paso 4: Programar tareas con un CronJob Un CronJob tiene dos partes: la programación (`schedule`) y una plantilla de Job (`jobTemplate`) que se usa en cada ejecución. La expresión `schedule` usa los cinco campos clásicos de cron: | Campo | Rango | Ejemplo | |-------|-------|---------| | Minuto | 0-59 | `*/15` cada 15 minutos | | Hora | 0-23 | `2` a las 02:00 | | Día del mes | 1-31 | `1` el día 1 | | Mes | 1-12 | `*` todos los meses | | Día de la semana | 0-6 (0 es domingo) | `1-5` de lunes a viernes | Algunas expresiones habituales: `0 2 * * *` (todos los días a las 02:00), `*/15 * * * *` (cada 15 minutos), `0 9 * * 1-5` (días laborables a las 09:00) y `0 3 1 * *` (el día 1 de cada mes a las 03:00). También se admiten las macros `@hourly`, `@daily`, `@weekly` y `@monthly`. Crea un CronJob que se ejecuta cada minuto para verlo funcionar rápido: ```bash nano heartbeat-cronjob.yaml ``` ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: heartbeat spec: schedule: "* * * * *" jobTemplate: spec: template: spec: containers: - name: heartbeat image: busybox:1.36 command: ["sh", "-c", "date; echo 'Tarea programada ejecutada'"] restartPolicy: OnFailure ``` ```bash kubectl apply -f heartbeat-cronjob.yaml kubectl get cronjob heartbeat ``` ```output NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE heartbeat * * * * * False 0 5s ``` Espera un par de minutos y lista los Jobs que ha creado. Cada nombre lleva un sufijo derivado de la hora programada: ```bash kubectl get jobs ``` ```output NAME STATUS COMPLETIONS DURATION AGE heartbeat-29311562 Complete 1/1 4s 72s heartbeat-29311563 Complete 1/1 4s 12s ``` No hace falta esperar a la siguiente ejecución para probar un CronJob: puedes crear un Job a partir de él en cualquier momento. ```bash kubectl create job --from=cronjob/heartbeat heartbeat-manual kubectl logs job/heartbeat-manual ``` ```output Thu Sep 24 10:31:17 UTC 2026 Tarea programada ejecutada ``` Borra este CronJob de prueba antes de continuar: ```bash kubectl delete cronjob heartbeat kubectl delete job heartbeat-manual ``` ## Paso 5: Ajustar zona horaria, concurrencia e historial Por defecto el horario se interpreta en la zona horaria del `kube-controller-manager`, que casi siempre es UTC. Estos campos controlan el comportamiento del CronJob: | Campo | Qué hace | |-------|----------| | `timeZone` | Zona horaria IANA para `schedule`, por ejemplo `Europe/Madrid`. Tiene en cuenta el horario de verano | | `concurrencyPolicy` | `Allow` (por defecto) permite ejecuciones solapadas, `Forbid` salta la nueva si la anterior sigue activa, `Replace` cancela la anterior | | `startingDeadlineSeconds` | Si el controlador no pudo lanzar el Job a su hora, segundos de margen para lanzarlo tarde; pasado ese margen se salta | | `successfulJobsHistoryLimit` | Jobs correctos que se conservan (por defecto `3`) | | `failedJobsHistoryLimit` | Jobs fallidos que se conservan (por defecto `1`) | | `suspend` | `true` detiene las ejecuciones futuras sin borrar el CronJob | Para tareas que no deben solaparse, como copias de seguridad, usa siempre `concurrencyPolicy: Forbid`. Lo verás aplicado en el siguiente paso. Para pausar un CronJob durante un mantenimiento y reanudarlo después: ```bash kubectl patch cronjob -p '{"spec":{"suspend":true}}' kubectl patch cronjob -p '{"spec":{"suspend":false}}' ``` ## Paso 6: Crear una copia diaria de PostgreSQL Este ejemplo junta todo lo anterior: un CronJob que cada noche a las 02:30 (hora de Madrid) ejecuta `pg_dump`, guarda el resultado comprimido en un volumen persistente y borra las copias de más de siete días. Primero guarda las credenciales en un Secret. Sustituye los valores por los de tu base de datos; `your_strong_password` es un marcador: ```bash kubectl create secret generic pg-backup-credentials \ --from-literal=PGHOST=postgres.databases.svc.cluster.local \ --from-literal=PGUSER=backup \ --from-literal=PGPASSWORD=your_strong_password \ --from-literal=PGDATABASE=app ``` `pg_dump` lee directamente las variables `PGHOST`, `PGUSER`, `PGPASSWORD` y `PGDATABASE`, así que no hace falta pasarlas como argumentos. Crea el manifiesto con el volumen y el CronJob: ```bash nano pg-backup.yaml ``` ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pg-backups spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: batch/v1 kind: CronJob metadata: name: pg-backup spec: schedule: "30 2 * * *" timeZone: "Europe/Madrid" concurrencyPolicy: Forbid startingDeadlineSeconds: 600 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: backoffLimit: 2 activeDeadlineSeconds: 3600 template: spec: restartPolicy: Never containers: - name: pg-dump image: postgres:17 envFrom: - secretRef: name: pg-backup-credentials command: - /bin/bash - -c - | set -euo pipefail file="/backups/${PGDATABASE}-$(date +%Y%m%d-%H%M%S).sql.gz" pg_dump --no-owner | gzip > "$file" echo "Copia creada: $file ($(du -h "$file" | cut -f1))" find /backups -name '*.sql.gz' -mtime +7 -delete volumeMounts: - name: backups mountPath: /backups volumes: - name: backups persistentVolumeClaim: claimName: pg-backups ``` Algunos detalles importantes: - La versión de la imagen `postgres` debe ser igual o superior a la del servidor, porque `pg_dump` se niega a volcar servidores más nuevos que él. - `set -o pipefail` hace que el Job falle si `pg_dump` falla, aunque `gzip` termine bien. Sin él tendrías copias vacías marcadas como correctas. - Si no tienes StorageClass predeterminada, añade `storageClassName` al PVC. Aplica el manifiesto y lanza una ejecución manual para comprobarlo sin esperar a la noche: ```bash kubectl apply -f pg-backup.yaml kubectl create job --from=cronjob/pg-backup pg-backup-test kubectl wait --for=condition=complete job/pg-backup-test --timeout=300s kubectl logs job/pg-backup-test ``` ```output Copia creada: /backups/app-20260924-103512.sql.gz (4.2M) ``` Comprueba que el CronJob muestra la zona horaria correcta: ```bash kubectl get cronjob pg-backup ``` ```output NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE pg-backup 30 2 * * * Europe/Madrid False 0 1m ``` > Importante: una copia guardada en el mismo clúster no te protege si pierdes el clúster. Copia periódicamente el contenido del volumen a otro lugar, por ejemplo a un almacenamiento de objetos compatible con S3. ## Solución de problemas **El Job no crea pods.** Revisa los eventos con `kubectl describe job `. Las causas típicas son una `ResourceQuota` agotada en el namespace o un error de validación en la plantilla. **Los pods se quedan en `Pending` o `ImagePullBackOff`.** No es un problema del Job sino del pod: `kubectl describe pod ` muestra si faltan recursos en los nodos, si el PVC no se enlaza o si la imagen no existe. **El CronJob no se ejecuta a la hora esperada.** Comprueba `timeZone` y que `suspend` sea `False` con `kubectl get cronjob`. Si el controlador estuvo caído más tiempo que `startingDeadlineSeconds`, esa ejecución se salta y aparece un evento en `kubectl describe cronjob `. **Se acumulan Jobs antiguos.** Los Jobs creados por un CronJob se limpian según `successfulJobsHistoryLimit` y `failedJobsHistoryLimit`. Para Jobs creados a mano, añade `ttlSecondsAfterFinished`. ## Conclusión Has creado Jobs con reintentos, límites de tiempo y limpieza automática, has repartido trabajo entre pods en paralelo con índices y has programado una copia diaria de PostgreSQL con zona horaria y sin solapamientos. Como siguientes pasos puedes enviar a un sistema de alertas los Jobs fallidos (por ejemplo con kube-state-metrics y Prometheus), sacar las copias del clúster a un almacenamiento externo o usar `podFailurePolicy` para no reintentar errores que nunca se van a resolver solos.