Kubernetes

CronJob programa un pod. No orquesta nada.

Kubernetes ya programa. Lo que no hace es expresar orden entre jobs, conservar los logs una vez recogido el pod, ni alcanzar nada fuera del clúster. Dagu ejecuta cada paso como un Job de Kubernetes y añade alrededor el grafo, el historial y el cruce de fronteras.

Cada paso es un Job de Kubernetes real, no un script alrededor de kubectl
Los logs del pod entran en el historial de Dagu y sobreviven al pod
Un workflow puede abarcar varios clústeres mediante contextos de kubeconfig
Los pasos de clúster conviven con SSH, HTTP y aprobación en el mismo grafo
01

Lo que CronJob no hace

Como planificador de un pod, CronJob está bien. Las carencias aparecen en cuanto un batch es más de un pod, y todos los equipos se topan con las mismas tres.

  • No hay dependencia entre CronJobs. El orden se expresa adivinando desfases de reloj, algo que se rompe en silencio el primer día que un job tarda más
  • El historial es escaso por defecto: se conservan tres ejecuciones correctas y una fallida, y los logs desaparecen con los pods. Explicar el fallo del martes pasado suele ser imposible
  • Si se pierden más de cien horarios consecutivos y startingDeadlineSeconds no está definido, el CronJob deja de programar por completo y espera a que alguien se dé cuenta
02

Los pasos son Jobs, y los logs vuelven

Dagu crea un Job de un solo contenedor por paso, lo espera, transmite los logs del pod y usa el código de salida del contenedor terminado. Los logs se escriben en el historial de ejecución, así que siguen ahí cuando el pod y el Job ya no están.

  • depends expresa el orden directamente, de modo que una extracción fallida detiene la carga en lugar de alimentarla con nada
  • retry_policy reintenta el paso creando un Job nuevo, que no es lo mismo que backoffLimit reiniciando un pod en el sitio
  • Los Jobs se eliminan al terminar por defecto, así que un grafo nocturno no deja tras de sí un rastro de Jobs finalizados

Kubernetes expone un flujo de logs de contenedor unificado, así que en este tipo de paso stdout y stderr llegan entrelazados como un único flujo.

Un batch de tres pasos como Jobs de Kubernetes reales
# k8s-nightly-etl.yaml
schedule: "0 2 * * *"
max_active_runs: 1

kubernetes:
  namespace: batch
  service_account: dagu-runner
  resources:
    requests:
      cpu: "250m"
      memory: "512Mi"

steps:
  - id: extract
    action: k8s.run
    with:
      image: ghcr.io/example/extract:1.4.0
      command: extract --source warehouse
    retry_policy:
      limit: 2
      interval_sec: 120

  - id: transform
    action: k8s.run
    with:
      image: ghcr.io/example/transform:1.4.0
    depends: extract

  - id: load
    action: k8s.run
    with:
      image: ghcr.io/example/load:1.4.0
      resources:
        limits:
          cpu: "2"
          memory: "4Gi"
    depends: transform

handler_on:
  failure:
    run: ./scripts/page-oncall.sh

mail_on:
  failure: true
03

El trabajo que no cabe en un clúster

Esta es la parte que CronJob no puede hacer con ningún esfuerzo, porque un CronJob está acotado al clúster donde vive. Un workflow que toca dos clústeres, o un clúster y un sistema on-premise, no tiene dónde existir dentro de Kubernetes.

  • kubeconfig y context se fijan por paso, así que preproducción y producción son dos pasos de un workflow en vez de dos despliegues del mismo manifiesto
  • Un paso de clúster puede situarse entre un paso SSH en un host heredado y una llamada HTTP a una API SaaS, porque el orquestador no es en sí un recurso del clúster
  • Un paso human.task pausa el workflow para una aprobación tipada, sin ocupar proceso mientras espera, y luego continúa al paso de producción
Promover una migración entre clústeres, con aprobación
# k8s-promote-across-clusters.yaml
max_active_runs: 1

kubernetes:
  namespace: batch
  service_account: dagu-runner

steps:
  - id: migrate_staging
    action: k8s.run
    with:
      context: staging
      image: ghcr.io/example/migrator:3.2
      command: migrate up

  - id: verify_staging
    run: ./scripts/verify-schema.sh staging
    depends: migrate_staging

  - id: approve
    action: human.task
    with:
      prompt: Promote the migration to production?
      form:
        type: object
        properties:
          change_ticket:
            type: string
          confirmed:
            type: boolean
        required: [change_ticket, confirmed]
    depends: verify_staging

  - id: migrate_production
    action: k8s.run
    with:
      context: production
      image: ghcr.io/example/migrator:3.2
      command: migrate up
    depends: approve

  - id: record
    run: ./scripts/record-change.sh "${steps.approve.outputs.change_ticket}"
    depends: migrate_production

handler_on:
  failure:
    run: ./scripts/page-oncall.sh

mail_on:
  failure: true
04

Dónde Argo Workflows es la mejor respuesta

Argo Workflows es la respuesta nativa de Kubernetes al mismo problema, y es la correcta para toda una clase de trabajo. La diferencia está en dónde vive el orquestador.

  • Argo corre como CRD y controlador dentro del clúster: una ventaja si todo lo que orquestas ya está ahí, y una restricción si no
  • Dagu es un binario y puede situarse fuera del clúster, que es lo que hace posibles los grafos entre clústeres y a través de fronteras
  • Si quieres que las definiciones de workflow sean objetos de Kubernetes, gestionados por el mismo RBAC y flujo GitOps que el resto del clúster, ese es el diseño de Argo y no el de Dagu
05

Lo que el ejecutor deliberadamente no expone

El tipo de paso de Kubernetes es estrecho a propósito. Cubre bien la forma habitual de un paso batch y no pretende ser un aplicador general de manifiestos.

  • Un contenedor por paso, con un solo comando. No hay paso directo de specs de Pod o Job en crudo
  • No se exponen parallelism, completions, completion mode ni success policy de los Jobs, así que los Jobs indexados y paralelos quedan fuera de alcance
  • restart_policy no es configurable, y las opciones específicas de Windows, SELinux, AppArmor y proc_mount no están disponibles

Si un campo no está documentado para este tipo de paso, dalo por no soportado. El trabajo que necesita una spec de Job completa se aplica mejor con kubectl desde un paso de comando, o se deja a Argo.

FAQ

Practical questions before adopting

¿Dagu tiene que ejecutarse dentro del clúster?

No. Resuelve el clúster con una kubeconfig explícita, luego con las reglas normales de carga y finalmente con la configuración in-cluster, así que funciona de ambas formas. Ejecutarlo fuera es lo que hace posibles los workflows entre clústeres y a través de fronteras; ejecutarlo dentro está bien cuando todo lo que toca está en ese clúster.

¿En qué se diferencia de un CronJob que ejecuta un script con kubectl?

Ese patrón te da orden y nada más: el código de salida del pod envoltorio oculta qué etapa falló, los reintentos rehacen todo el script y los logs son un flujo indiferenciado que desaparece con el pod. Dagu crea un Job por paso, así que cada etapa tiene su estado, su reintento y su log conservado.

¿Los pasos comparten sistema de ficheros?

No. Cada paso es un Job distinto y, por tanto, un pod distinto. Pasa datos entre pasos con almacenamiento de objetos, un volumen persistente montado en cada paso, o salidas de paso para valores pequeños, igual que harías entre Jobs separados.

¿Qué ocurre con el Job cuando se cancela una ejecución?

Las rutas de cancelación, terminación y timeout fuerzan la limpieza incluso con cleanup_policy en keep, así que una ejecución detenida no deja el Job atrás. En funcionamiento normal, lo predeterminado es borrar el Job al terminar.

¿Deberíamos usar esto en lugar de Argo Workflows?

Solo si te resulta útil que el orquestador esté fuera del clúster. Si todas las tareas son contenedores de un mismo clúster y quieres que los workflows sean objetos de Kubernetes bajo el mismo RBAC y GitOps, Argo encaja mejor. Dagu sirve para grafos que mezclan jobs de clúster con hosts, APIs y aprobaciones humanas.

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.