Kubernetes

O CronJob agenda um pod. Não orquestra coisa alguma.

O Kubernetes já agenda. O que não faz é exprimir ordem entre jobs, manter os logs depois de o pod ser recolhido, ou alcançar algo fora do cluster. O Dagu executa cada passo como um Job do Kubernetes e acrescenta em redor o grafo, o histórico e a travessia de fronteiras.

Cada passo é um Job do Kubernetes a sério, não um script à volta do kubectl
Os logs do pod entram no histórico do Dagu e sobrevivem ao pod
Um workflow pode abranger vários clusters através de contextos de kubeconfig
Passos de cluster convivem com SSH, HTTP e aprovação no mesmo grafo
01

O que o CronJob não faz

Como agendador de um pod, o CronJob é bom. As lacunas aparecem assim que um lote é mais do que um pod, e todas as equipas batem nas mesmas três.

  • Não existe dependência entre CronJobs. A ordem é expressa adivinhando desfasamentos de relógio, o que quebra em silêncio no primeiro dia em que um job demora mais
  • O histórico é raso por omissão: ficam três execuções bem sucedidas e uma falhada, e os logs desaparecem com os pods. Explicar a falha da terça passada é muitas vezes impossível
  • Se forem perdidos mais de cem horários consecutivos e startingDeadlineSeconds não estiver definido, o CronJob deixa de agendar por completo e espera que alguém repare
02

Os passos são Jobs, e os logs voltam

O Dagu cria um Job de um só contentor por passo, espera por ele, transmite os logs do pod e usa o código de saída do contentor terminado. Os logs são escritos no histórico de execução, pelo que continuam lá quando o pod e o Job já desapareceram.

  • depends exprime a ordem diretamente, por isso uma extração falhada trava o carregamento em vez de o alimentar com nada
  • retry_policy repete o passo criando um Job novo, o que é diferente de o backoffLimit reiniciar um pod no lugar
  • Os Jobs são eliminados após a conclusão por omissão, portanto um grafo noturno não deixa atrás de si um rasto de Jobs terminados

O Kubernetes expõe um fluxo de logs de contentor unificado, pelo que neste tipo de passo o stdout e o stderr chegam entrelaçados como um único fluxo.

Um lote de três passos como Jobs reais do Kubernetes
# 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

O trabalho que não cabe num cluster

Esta é a parte que o CronJob não consegue fazer a nenhum custo, porque um CronJob está limitado ao cluster onde vive. Um workflow que toca dois clusters, ou um cluster e um sistema local, não tem onde existir dentro do Kubernetes.

  • kubeconfig e context definem-se por passo, pelo que pré-produção e produção são dois passos de um workflow em vez de duas implantações do mesmo manifesto
  • Um passo de cluster pode ficar entre um passo SSH num sistema antigo e uma chamada HTTP a uma API SaaS, porque o orquestrador não é ele próprio um recurso do cluster
  • Um passo human.task suspende o workflow para uma aprovação tipada, sem ocupar processo enquanto espera, e depois segue para o passo de produção
Promover uma migração entre clusters, com aprovação
# 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

Onde o Argo Workflows é a melhor resposta

O Argo Workflows é a resposta nativa do Kubernetes ao mesmo problema, e é a correta para uma grande classe de trabalho. A diferença está em onde vive o orquestrador.

  • O Argo corre como CRDs e controlador dentro do cluster, uma vantagem se tudo o que orquestra já lá está, e uma restrição se não estiver
  • O Dagu é um binário e pode ficar fora do cluster, o que é precisamente o que torna possíveis os grafos entre clusters e através de fronteiras
  • Se quiser que as definições de workflow sejam objetos do Kubernetes, geridos pelo mesmo RBAC e fluxo GitOps do resto do cluster, esse é o desenho do Argo e não o do Dagu
05

O que o executor deliberadamente não expõe

O tipo de passo Kubernetes é estreito de propósito. Cobre bem a forma habitual de um passo de lote e não tenta ser um aplicador geral de manifestos.

  • Um contentor por passo, com um único comando. Não há passagem direta de specs de Pod ou Job em bruto
  • parallelism, completions, completion mode e success policy dos Jobs não são expostos, pelo que Jobs indexados e paralelos ficam fora do âmbito
  • restart_policy não é configurável, e opções específicas do Windows, SELinux, AppArmor e proc_mount não estão disponíveis

Se um campo não estiver documentado para este tipo de passo, assuma que não é suportado. Trabalho que exija uma spec de Job completa aplica-se melhor com kubectl a partir de um passo de comando, ou deixa-se ao Argo.

FAQ

Practical questions before adopting

O Dagu tem de correr dentro do cluster?

Não. Resolve o cluster através de uma kubeconfig explícita, depois das regras normais de carregamento e por fim da configuração in-cluster, pelo que funciona nos dois casos. Corrê-lo fora é o que torna possíveis os workflows entre clusters e através de fronteiras; corrê-lo dentro está bem quando tudo o que toca está nesse cluster.

Em que difere de um CronJob que corre um script a chamar kubectl?

Esse padrão dá-lhe ordem e mais nada: o código de saída do pod invólucro esconde qual a fase que falhou, as repetições refazem o script inteiro e os logs são um fluxo indiferenciado que desaparece com o pod. O Dagu cria um Job por passo, pelo que cada fase tem o seu estado, a sua repetição e o seu log guardado.

Os passos partilham um sistema de ficheiros?

Não. Cada passo é um Job distinto e, portanto, um pod distinto. Passe dados através de armazenamento de objetos, de um volume persistente montado em cada passo, ou de saídas de passo para valores pequenos, tal como faria entre Jobs separados.

O que acontece ao Job quando uma execução é cancelada?

Os caminhos de cancelamento, terminação e timeout forçam a limpeza mesmo com cleanup_policy em keep, pelo que uma execução parada não deixa o Job para trás. Em funcionamento normal, o Job é eliminado assim que termina.

Devemos usar isto em vez do Argo Workflows?

Só se lhe for útil que o orquestrador esteja fora do cluster. Se todas as tarefas forem contentores num único cluster e quiser workflows como objetos do Kubernetes sob o mesmo RBAC e GitOps, o Argo encaixa melhor. O Dagu serve grafos que misturam jobs de cluster com hosts, APIs e aprovações 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.