Los procesos por lotes no deberían administrarse como aplicaciones que deben permanecer activas. Kubernetes ofrece dos recursos específicos: un Job ejecuta una tarea hasta completarla y un CronJob crea Jobs según un calendario.
La confiabilidad no consiste solamente en programar un comando. También requiere limitar reintentos y duración, evitar ejecuciones simultáneas no deseadas, conservar evidencia y diseñar la tarea para que pueda repetirse sin duplicar efectos.

Qué vas a lograr
- Ejecutar una tarea puntual con límites de tiempo y reintentos.
- Programar esa tarea en UTC sin permitir superposiciones.
- Validar los manifiestos antes de persistirlos.
- Comprobar condiciones, Pods, eventos y logs.
- Suspender la programación como medida de contención.
Cómo se relacionan CronJob, Job y Pod
El CronJob no ejecuta el contenedor directamente. Cuando llega el horario configurado, crea un Job. El controlador de Jobs crea uno o más Pods y observa si la tarea termina correctamente.
La creación programada es aproximada: en determinadas condiciones puede omitirse o duplicarse una ejecución. Por eso, la operación debe ser idempotente. Repetirla no debería duplicar cobros, envíos, registros ni cambios irreversibles.
Requisitos previos
- Clúster Kubernetes accesible y
kubectlconfigurado. - Permisos para administrar
jobs.batch,cronjobs.batch, Pods, eventos y logs en un namespace de prueba. - Imagen de contenedor probada, con un comando que termine y devuelva código
0al completarse. - Política definida para reintentos, tiempo máximo, concurrencia, historial y zona horaria.
- Backup y mecanismo de idempotencia cuando la tarea modifique datos.
Los ejemplos utilizan donweb-lab y busybox:1.36.1. En producción, fija las imágenes por digest.
Paso 1: comprobar el contexto y los permisos
kubectl config current-context
kubectl version --client
kubectl cluster-info
kubectl create namespace donweb-lab \
--dry-run=client -o yaml | kubectl apply -f -
kubectl auth can-i create jobs.batch -n donweb-lab
kubectl auth can-i create cronjobs.batch -n donweb-lab
kubectl auth can-i get pods/log -n donweb-labLas consultas de autorización deberían devolver yes. Si alguna devuelve no, ajusta RBAC; no uses credenciales administrativas como solución permanente.
Paso 2: configurar un Job con límites explícitos
Guarda el siguiente manifiesto como job-exportacion.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: exportacion-unica
namespace: donweb-lab
spec:
backoffLimit: 3
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: exportacion
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: tarea
image: busybox:1.36.1
command:
- /bin/sh
- -c
- 'date -u; echo "exportacion completada"'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64MibackoffLimit limita los reintentos. activeDeadlineSeconds corta toda la ejecución al superar diez minutos, aunque todavía queden reintentos. ttlSecondsAfterFinished elimina automáticamente el Job y sus Pods una hora después de finalizar.
Paso 3: validar antes de aplicar
kubectl apply --dry-run=client -f job-exportacion.yaml
kubectl apply --dry-run=server -f job-exportacion.yaml
kubectl diff -f job-exportacion.yaml--dry-run=server comprueba esquema, admisión y políticas contra la API sin guardar el recurso. kubectl diff puede devolver código 1 cuando detecta diferencias; esto no significa por sí mismo que exista un error.
Después de revisar los cambios:
kubectl apply -f job-exportacion.yamlPaso 4: esperar y comprobar el resultado
kubectl wait --for=condition=complete \
job/exportacion-unica -n donweb-lab --timeout=11m
kubectl get job/exportacion-unica -n donweb-lab
kubectl get pods -n donweb-lab \
-l job-name=exportacion-unica -o wide
kubectl logs job/exportacion-unica -n donweb-labEl Job debe mostrar una ejecución completada y el log debe contener exportacion completada. Si vence la espera, revisa eventos y logs antes de volver a aplicar el manifiesto.
Paso 5: configurar un CronJob inicialmente suspendido
Guarda como cronjob-exportacion.yaml:
apiVersion: batch/v1
kind: CronJob
metadata:
name: exportacion-diaria
namespace: donweb-lab
spec:
schedule: "0 2 * * *"
timeZone: "Etc/UTC"
suspend: true
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 3
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: tarea
image: busybox:1.36.1
command:
- /bin/sh
- -c
- 'date -u; echo "exportacion programada completada"'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64MiLa tarea se programa todos los días a las 02:00 UTC. Forbid evita iniciar otra ejecución mientras la anterior sigue activa. startingDeadlineSeconds: 300 admite hasta cinco minutos de retraso.
Evita valores inferiores a diez segundos: el controlador comprueba periódicamente los horarios y una ventana demasiado corta puede impedir la ejecución. En clústeres anteriores a Kubernetes 1.27, verifica la compatibilidad de spec.timeZone.
Valida y aplica sin habilitar todavía el calendario:
kubectl apply --dry-run=server -f cronjob-exportacion.yaml
kubectl diff -f cronjob-exportacion.yaml
kubectl apply -f cronjob-exportacion.yaml
kubectl get cronjob/exportacion-diaria -n donweb-labPaso 6: ejecutar una prueba manual
JOB="exportacion-prueba-$(date -u +%Y%m%d%H%M%S)"
kubectl create job "$JOB" \
--from=cronjob/exportacion-diaria -n donweb-lab
kubectl wait --for=condition=complete \
"job/$JOB" -n donweb-lab --timeout=11m
kubectl logs "job/$JOB" -n donweb-lab
kubectl describe "job/$JOB" -n donweb-labVer Imagen 2: “Validar antes de programar”.
Paso 7: habilitar y observar el CronJob
kubectl patch cronjob/exportacion-diaria -n donweb-lab \
--type=merge -p '{"spec":{"suspend":false}}'
kubectl get cronjob/exportacion-diaria -n donweb-lab
kubectl get jobs -n donweb-lab --watchComprueba SUSPEND, LAST SCHEDULE y el próximo horario. Modificar un CronJob solo afecta a Jobs futuros; los que ya comenzaron conservan la configuración anterior.
Contención y reversión
Para impedir nuevas ejecuciones sin interrumpir la activa:
kubectl patch cronjob/exportacion-diaria -n donweb-lab \
--type=merge -p '{"spec":{"suspend":true}}'Para detener un Job activo, primero evalúa el impacto de una escritura parcial:
kubectl get jobs -n donweb-lab
kubectl delete job NOMBRE_DEL_JOB -n donweb-labEliminar el CronJob no revierte cambios realizados en sistemas externos.
Problemas frecuentes
El Job no completa: revisa kubectl describe, los eventos, el estado de la imagen, límites de recursos, dependencias externas y código de salida.
kubectl get events -n donweb-lab \
--sort-by=.metadata.creationTimestamp
kubectl logs job/exportacion-unica -n donweb-lab --all-containersEl CronJob no crea Jobs: comprueba suspend, schedule, timeZone, startingDeadlineSeconds, eventos y hora del clúster.
Las ejecuciones se superponen: revisa concurrencyPolicy. Allow permite concurrencia, Forbid omite el nuevo horario y Replace sustituye la ejecución anterior.
La tarea se ejecuta dos veces: Kubernetes no garantiza exactamente una ejecución. Usa claves únicas, transacciones, bloqueos con vencimiento o escrituras condicionales.
Los Pods desaparecen demasiado pronto: amplía el TTL o envía logs a un sistema externo.
Buenas prácticas
- Utiliza una cuenta de servicio dedicada y RBAC mínimo.
- Mantén secretos fuera del manifiesto.
- Define solicitudes y límites de recursos.
- Alerta por fallos, duración anómala y ausencia de ejecuciones.
- Fija imágenes por digest.
- Registra identificador de ejecución, hora UTC, entrada y resultado.
Conclusión
Un Job confiable termina, informa su resultado y tiene límites claros. Un CronJob añade calendario, política de concurrencia y tolerancia a retrasos, pero no elimina la necesidad de idempotencia.
La secuencia recomendada es: configurar, validar contra la API, ejecutar una prueba manual, comprobar la evidencia y recién entonces habilitar el calendario. Más información en la documentación oficial sobre Jobs y CronJobs.
Acelerar builds Docker con la caché de BuildKit
BuildKit puede reutilizar capas, descargas de paquetes y resultados entre compilaciones. Sin embargo, tener BuildKit disponible no garantiza builds rápidos: el beneficio depende del orden del Dockerfile, del tamaño del contexto y de dónde se almacena la caché.
En un equipo local, la caché interna del builder suele ser suficiente. En CI, donde los runners suelen ser efímeros, hay que importar y exportar la caché explícitamente.
Qué vas a lograr
- Identificar el builder utilizado.
- Evitar invalidaciones innecesarias.
- Reducir el contexto con
.dockerignore. - Reutilizar descargas con
RUN --mount=type=cache. - Medir duración, aciertos y uso de disco.
- Compartir caché entre ejecuciones de CI.
Cómo funciona la caché
Una capa se reutiliza cuando coinciden la instrucción y las entradas de las que depende. Si cambia una capa, las instrucciones posteriores deben reconstruirse.
Por eso conviene copiar primero package.json y package-lock.json, ejecutar npm ci y copiar después el código. Un cambio en src/ no debería volver a descargar todas las dependencias.
Los montajes type=cache conservan directorios auxiliares, como /root/.npm, entre builds. Su contenido mejora el rendimiento, pero no forma parte de la imagen. El build debe funcionar incluso cuando esa caché está vacía.
Ver Imagen 3: “Qué invalida la caché de capas”.
Requisitos previos
- Docker Engine o Docker Desktop con
docker buildx. - Proyecto Node.js con
package.json,package-lock.json, scriptbuildy salida endist/. - Espacio disponible para la caché local.
- Para CI: registro OCI y permisos de lectura y escritura.
Paso 1: comprobar el builder
docker version
docker buildx version
docker buildx ls
docker buildx inspect --bootstrapCada builder mantiene su propia caché interna. Cambiar de builder puede producir un build sin aciertos aunque el Dockerfile no haya cambiado.
Si no existe un builder utilizable:
docker buildx create \
--name donweb-builder \
--driver docker-container \
--use --bootstrapNo reemplaces un builder compartido sin verificar quién lo utiliza.
Paso 2: reducir el contexto
Crea .dockerignore:
node_modules
.git
.gitignore
.env
.env.*
!.env.example
dist
coverage
npm-debug.log*
Dockerfile*
compose*.yamlRevisa las exclusiones según tu proyecto. Mantener .env fuera del contexto reduce el riesgo de copiar secretos, pero no sustituye un análisis del repositorio.
Paso 3: ordenar el Dockerfile y usar cache mounts
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM base AS prod-deps
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm ci --omit=dev
FROM base AS runtime
ENV NODE_ENV=production
USER node
COPY --from=prod-deps --chown=node:node \
/app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
CMD ["node", "dist/index.js"]El orden separa entradas estables y variables. npm ci depende de los manifiestos; COPY . . aparece después, por lo que modificar el código no invalida la instalación de dependencias.
En producción, fija la imagen base por digest.
Paso 4: validar y construir
docker buildx build --check .
docker buildx build \
--progress=plain \
--load \
-t app:cache-test .Si --check no está disponible, actualiza Buildx o realiza un build de prueba y revisa sus advertencias. --load es apropiado para una imagen de una sola plataforma que debe quedar disponible en el Docker local.
Paso 5: medir una segunda compilación
time docker buildx build --progress=plain --load \
-t app:cache-test . 2>&1 | tee build-1.log
time docker buildx build --progress=plain --load \
-t app:cache-test . 2>&1 | tee build-2.log
grep -E 'CACHED|DONE' build-2.log
docker buildx duLa segunda ejecución debería mostrar CACHED en los pasos cuyas entradas no cambiaron. Repite la medición en el mismo entorno y compara medianas, no una única ejecución.
Paso 6: provocar una invalidación controlada
Modifica únicamente un archivo dentro de src/ y vuelve a construir:
time docker buildx build --progress=plain --load \
-t app:cache-test . 2>&1 | tee build-src-change.logLa instalación con npm ci debería seguir en caché. COPY . . y npm run build deben ejecutarse nuevamente.
Después modifica válidamente package-lock.json: la capa de dependencias debe reconstruirse. Esta prueba confirma que las fronteras de caché corresponden con las entradas reales.
Paso 7: compartir caché en CI
La caché interna pertenece al builder y no acompaña automáticamente a un runner nuevo. Utiliza un backend externo:
REGISTRY="registry.example.com/equipo"
IMAGE="$REGISTRY/app"
CACHE="$REGISTRY/app:buildcache-main"
TAG="${GIT_COMMIT:-manual}"
docker buildx build \
--push \
-t "$IMAGE:$TAG" \
--cache-from "type=registry,ref=$CACHE" \
--cache-to "type=registry,ref=$CACHE,mode=max" \
.--cache-from importa la caché existente. --cache-to publica el nuevo estado. mode=max incluye capas intermedias, lo que suele beneficiar a CI a cambio de mayor almacenamiento.
El driver docker admite determinados backends externos solo cuando está habilitado el almacén de imágenes de containerd. Si el backend no es compatible, verifica el driver con docker buildx ls o utiliza docker-container.
Para evitar que varias ramas sobrescriban la misma referencia:
BRANCH_CACHE="$REGISTRY/app:buildcache-${BRANCH_SAFE}"
docker buildx build --push -t "$IMAGE:$TAG" \
--cache-from "type=registry,ref=$BRANCH_CACHE" \
--cache-from "type=registry,ref=$CACHE" \
--cache-to "type=registry,ref=$BRANCH_CACHE,mode=max" \
.BRANCH_SAFE debe convertirse en una etiqueta válida y no debe contener información sensible.
Paso 8: gestionar secretos correctamente
No pases credenciales mediante ARG, ENV o COPY. Pueden quedar expuestas en capas, historial o metadatos.
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
--mount=type=secret,id=npmrc,target=/root/.npmrc,required=true \
npm ciEntrega el secreto únicamente durante el build:
docker buildx build \
--secret id=npmrc,src="$HOME/.npmrc" \
--load -t app:cache-test .En CI, crea el archivo temporal desde el almacén de secretos de la plataforma, restringe sus permisos y elimínalo al finalizar.
Reversión y limpieza
La caché es una optimización, no una dependencia funcional. El mismo Dockerfile debe funcionar con la caché vacía.
docker buildx dudocker buildx prune elimina caché y puede ralentizar builds posteriores. No lo automatices sin filtros y una política de retención.
Problemas frecuentes
No aparece CACHED: confirma que utilizas el mismo builder y que no cambiaron contexto, argumentos, plataforma o imagen base.
npm ci se ejecuta ante cada cambio: coloca package.json y el lockfile antes de COPY . ..
La caché remota no se exporta: revisa driver, permisos del registro, espacio y referencia.
La caché crece demasiado: define retención y separa cachés por rama o alcance. mode=max consume más almacenamiento.
El resultado cambia al usar caché: fija versiones, lockfiles e imágenes. Evita descargar contenidos “latest” durante un RUN.
Buenas prácticas
- Utiliza lockfiles y digests reproducibles.
- Mantén pequeño el contexto.
- Separa dependencias del código que cambia frecuentemente.
- Usa cache mounts apropiados para cada gestor.
- Entrega secretos mediante
--secreto--ssh. - Registra duración, aciertos y tamaño de caché.
- Separa referencias de caché por rama.