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.


orden optimizadoCI r

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 kubectl configurado.
  • 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 0 al 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-lab

Las 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: 64Mi

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

Paso 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-lab

El 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: 64Mi

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

Paso 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-lab
Ver 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 --watch

Comprueba 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-lab

Eliminar 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-containers

El 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, script build y salida en dist/.
  • 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 --bootstrap

Cada 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 --bootstrap

No 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*.yaml

Revisa 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 du

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

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

Entrega 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 du

docker 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 --secret o --ssh.
  • Registra duración, aciertos y tamaño de caché.
  • Separa referencias de caché por rama.

Cloud Serversby Donweb

Todo el poder de la nube a tus proyectos y aplicaciones.
Alojamiento ultra rápido, escalable y con alta disponibilidad.

  • Performance que te sorprenderá
  • Rápida escalabilidad y sin limitaciones
  • Arquitectura de alta disponibilidad
  • Soporte experto y ejecutivos de cuenta
  • Pagos en tu moneda y facturación local

Descubre la mejor solución de Cloud Hosting