Como Usar DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote em Produção Já leu

DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote Quando você começa a trabalhar com Kubernetes em ambientes de produção, logo percebe que nem toda aplicação funciona como um deployment tradicional. Existem cenários onde você precisa que um processo execute em cada nó do cluster (como coleta de logs ou monitoramento), ou então precisa executar tarefas pontuais que terminam (como backup ou processamento em lote). Para esses casos, Kubernetes oferece duas primitivas poderosas: DaemonSets e Jobs. Neste artigo, vou te ensinar não apenas o "como fazer", mas o quando e por quê usar cada um deles. DaemonSets: Agentes que Rodam em Todo Lugar O Conceito Fundamental Um DaemonSet garante que uma cópia específica de um Pod seja executada em cada nó do seu cluster Kubernetes. Imagine que você precisa coletar logs de CPU, memória ou eventos de cada máquina. Se você tivesse apenas um Pod fazendo isso, você perderia informações dos outros nós. Um DaemonSet resolve esse problema de

DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote

Quando você começa a trabalhar com Kubernetes em ambientes de produção, logo percebe que nem toda aplicação funciona como um deployment tradicional. Existem cenários onde você precisa que um processo execute em cada nó do cluster (como coleta de logs ou monitoramento), ou então precisa executar tarefas pontuais que terminam (como backup ou processamento em lote). Para esses casos, Kubernetes oferece duas primitivas poderosas: DaemonSets e Jobs. Neste artigo, vou te ensinar não apenas o "como fazer", mas o quando e por quê usar cada um deles.

DaemonSets: Agentes que Rodam em Todo Lugar

O Conceito Fundamental

Um DaemonSet garante que uma cópia específica de um Pod seja executada em cada nó do seu cluster Kubernetes. Imagine que você precisa coletar logs de CPU, memória ou eventos de cada máquina. Se você tivesse apenas um Pod fazendo isso, você perderia informações dos outros nós. Um DaemonSet resolve esse problema de forma elegante: automaticamente, quando um novo nó entra no cluster, um Pod é criado nele. Quando um nó é removido, o Pod associado também é limpo.

A principal diferença entre um DaemonSet e um Deployment está justamente na semântica: um Deployment diz "eu quero 3 réplicas espalhadas pelo cluster", enquanto um DaemonSet diz "eu quero uma réplica em cada nó". DaemonSets são ideais para ferramentas de infraestrutura, não para aplicações de negócio convencionais.

Caso de Uso Real

Considere um cenário comum: você precisa de um agente de monitoramento (como o Prometheus Node Exporter ou Datadog Agent) em cada máquina do seu cluster. Manualmente manter isso seria um pesadelo em um cluster que cresce constantemente. Um DaemonSet faz isso automaticamente.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      tolerations:
      # Este tolerationAllowsSchedulingTaintsTolerateAllNodesDefault faz o DaemonSet
      # rodar até em nós que têm taints (como nós master, se configurado assim)
      - effect: NoSchedule
        operator: Exists
      containers:
      - name: node-exporter
        image: prom/node-exporter:latest
        ports:
        - containerPort: 9100
        volumeMounts:
        - name: proc
          mountPath: /host/proc
          readOnly: true
        - name: sys
          mountPath: /host/sys
          readOnly: true
      volumes:
      - name: proc
        hostPath:
          path: /proc
      - name: sys
        hostPath:
          path: /sys

Neste exemplo, o DaemonSet garante que o Node Exporter rode em cada nó. Repare nas duas configurações importantes: tolerations (para permitir que rode em nós com taints especiais) e hostPath volumes (para acessar informações do nó hospedeiro).

Seletores e Node Affinity

Às vezes você não quer que um DaemonSet rode em todos os nós. Por exemplo, você pode ter nós GPU que precisam de um agente especial, mas os nós comuns não. Para isso, usamos nodeSelector ou affinity:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: gpu-monitor
spec:
  selector:
    matchLabels:
      app: gpu-monitor
  template:
    metadata:
      labels:
        app: gpu-monitor
    spec:
      nodeSelector:
        gpu: "true"
      containers:
      - name: gpu-monitor
        image: nvidia/cuda:11.0-base
        command: ["nvidia-smi"]

Aqui, o DaemonSet só será criado em nós que têm o label gpu=true. Se você adicionar esse label a um nó, automaticamente um Pod será criado lá. Se remover, o Pod será removido.

Jobs: Tarefas que Precisam Terminar

Entendendo a Natureza do Job

Um Job em Kubernetes representa uma tarefa com início e fim. Ao contrário de um Pod em um Deployment (que é restartado indefinidamente), um Job assume que seu trabalho será concluído e o Pod pode terminar com sucesso. Um Job é responsável por garantir que o trabalho seja feito: se um Pod falha, o Job cria outro. Se o Pod termina com sucesso, o Job marca a tarefa como completa.

Jobs são perfeitos para: backup de banco de dados, processamento em lote de arquivos, geração de relatórios, limpeza de dados ou qualquer tarefa que tenha um começo e um fim definidos.

Um Job Simples

apiVersion: batch/v1
kind: Job
metadata:
  name: backup-database
spec:
  backoffLimit: 3  # Tenta até 3 vezes se falhar
  completions: 1  # Número de Pods que precisam terminar com sucesso
  parallelism: 1  # Número máximo de Pods rodando simultaneamente
  ttlSecondsAfterFinished: 3600  # Delete o Job 1 hora após terminar
  template:
    spec:
      restartPolicy: Never  # Não reinicia Pods com falha (deixa o Job fazer retry)
      containers:
      - name: backup
        image: postgres:13
        env:
        - name: PGHOST
          value: "postgres-svc"
        - name: PGUSER
          value: "postgres"
        - name: PGPASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        command: ["sh", "-c"]
        args:
        - |
          pg_dump --host=$PGHOST --username=$PGUSER --format=custom mydb > /backup/mydb-$(date +%s).dump
          if [ $? -eq 0 ]; then
            echo "Backup concluído com sucesso"
            exit 0
          else
            echo "Backup falhou"
            exit 1
          fi
        volumeMounts:
        - name: backup-storage
          mountPath: /backup
      volumes:
      - name: backup-storage
        persistentVolumeClaim:
          claimName: backup-pvc

Neste exemplo, o Job tenta executar um backup de banco de dados. Se falhar, vai tentar até 3 vezes (backoffLimit: 3). Após terminar com sucesso, o Job é automaticamente deletado após 1 hora (ttlSecondsAfterFinished). A chave aqui é que o Pod não é reiniciado indefinidamente — o Job controla os retry.

CronJobs: Jobs Agendados

Às vezes você precisa que um Job execute periodicamente. Para isso existe o CronJob, que é essencialmente um agendador de Jobs:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-cleanup
spec:
  schedule: "0 2 * * *"  # Roda diariamente às 2 da manhã (UTC)
  jobTemplate:
    spec:
      backoffLimit: 2
      ttlSecondsAfterFinished: 86400
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: cleanup
            image: python:3.9-slim
            command: ["python"]
            args:
            - |
              import os
              import glob
              from datetime import datetime, timedelta

              cleanup_dir = "/data/uploads"
              max_age_days = 30
              cutoff_date = datetime.now() - timedelta(days=max_age_days)

              for filepath in glob.glob(f"{cleanup_dir}/*"):
                  file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
                  if file_mtime < cutoff_date:
                      os.remove(filepath)
                      print(f"Removido: {filepath}")
            volumeMounts:
            - name: data
              mountPath: /data
          volumes:
          - name: data
            persistentVolumeClaim:
              claimName: data-pvc
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

O CronJob aqui executa diariamente e remove arquivos com mais de 30 dias. Repare que schedule usa a sintaxe padrão de cron do Unix. O successfulJobsHistoryLimit mantém os últimos 3 Jobs bem-sucedidos para auditoria, e failedJobsHistoryLimit mantém apenas o último Job que falhou.

Processamento em Paralelo com Jobs

Há casos onde você precisa processar muitos itens em paralelo. Um Job pode fazer isso com completions e parallelism:

apiVersion: batch/v1
kind: Job
metadata:
  name: image-processor
spec:
  completions: 100  # 100 items para processar
  parallelism: 10   # Processa 10 em paralelo
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: processor
        image: image-processor:latest
        env:
        - name: QUEUE_HOST
          value: "rabbitmq-svc"
        command: ["python", "/app/process.py"]

Esse Job vai criar 100 Pods no total, mas apenas 10 vão rodar simultaneamente. Conforme um Pod termina, outro é iniciado até que todos os 100 completem. É ideal para workloads que podem ser paralelizados.

Diferenças Críticas e Como Escolher

Comparação Direta

Aspecto DaemonSet Job
Propósito Agente que roda continuamente em cada nó Tarefa que executa e termina
Ciclo de vida Permanente (enquanto houver nós) Temporário (até completar)
Replicas Uma por nó Configurável (completions)
Restart Sim, se o Pod falhar Sim, até backoffLimit
Melhor para Monitoramento, logs, rede Batch, backup, limpeza

Heurística de Decisão

Faça-se essa pergunta: "Este processo deveria estar rodando o tempo todo, ou é uma tarefa que completa?"

Se a resposta é "o tempo todo, em cada lugar", use DaemonSet. Se é "execute uma tarefa e deixe ela terminar", use Job. Se é "execute uma tarefa regularmente", use CronJob.

Monitoramento e Observabilidade

Ambos os recursos podem ser monitorados através da API do Kubernetes:

# Ver DaemonSets
kubectl get daemonsets -A
kubectl describe daemonset node-exporter -n monitoring

# Ver Jobs
kubectl get jobs -A
kubectl describe job backup-database
kubectl logs job/backup-database

# Ver status de um CronJob
kubectl get cronjobs
kubectl get jobs --selector=cronjob-name=daily-cleanup

Para monitoramento em Prometheus, você pode criar um ServiceMonitor que scrape a métrica kube_job_status_failed e kube_daemonset_status_misscheduled para detectar problemas.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: kubernetes-jobs-alerts
spec:
  groups:
  - name: kubernetes.jobs
    rules:
    - alert: JobFailed
      expr: kube_job_status_failed{job=~".*"} > 0
      for: 5m
      annotations:
        summary: "Job {{ $labels.job_name }} falhou"
    - alert: DaemonSetMisscheduled
      expr: kube_daemonset_status_misscheduled > 0
      annotations:
        summary: "DaemonSet {{ $labels.daemonset }} com Pods não agendados"

Conclusão

Os três aprendizados principais que você deve carregar daqui:

  1. DaemonSets são para infraestrutura contínua: quando você precisa de um agente rodando em cada máquina do cluster, DaemonSets eliminam a complexidade de gerenciar Pods manualmente. Use para monitoramento, coleta de logs, e qualquer ferramenta de sistema que precise estar "em todo lugar".

  2. Jobs são para tarefas discretas: a semântica fundamental é diferente — um Job não quer que você gerencie réplicas, quer que você defina quantas vezes a tarefa precisa completar com sucesso. Combine com CronJobs para agendamento e paralelismo para processamento em lote eficiente.

  3. Observabilidade é essencial: tanto DaemonSets quanto Jobs precisam de monitoramento ativo. Um DaemonSet com Pods não agendados é um sinal de alerta. Um Job que fica em estado "Failed" indefinidamente é um problema real que você só descobre com alertas.

Referências


Artigos relacionados