"모든 노드에 무조건 하나", "끝나고 죽는 Pod" — Deployment로 못 다루는 두 워크로드

로그 수집 에이전트를 띄우려 한다. 요구사항은 단순하다: "클러스터의 모든 노드에 정확히 하나씩". 노드가 5대면 5개, 10대가 되면 10개. Deployment로 replicas: 5를 하면? 노드가 10대로 늘어도 여전히 5개 — 한 노드에 2개가 몰리거나 어떤 노드엔 아예 없을 수 있다. 요구사항을 만족 못 한다.

또 다른 요구사항: "배치 작업을 돌려서 끝나면 사라지게". 그런데 Deployment의 Pod는 restartPolicy: Always영원히 재시작한다. "끝나면 그만"이라는 의미를 표현할 수가 없다.

이 글은 Deployment가 다루지 못하는 두 부류의 워크로드 — 노드 단위(DaemonSet)와 종료 지향(Job/CronJob) — 를 다룬다. 둘 다 "왜 Deployment가 아닌가"에서 시작한다.

DaemonSet — 노드마다 정확히 하나

DaemonSet모든 노드(또는 조건에 맞는 노드)에 Pod 복제본을 정확히 하나씩 띄운다. 노드가 클러스터에 추가되면 그 노드에도 자동으로 하나가 생기고, 노드가 제거되면 같이 사라진다.

flowchart TD
    DS["DaemonSet<br/>logging-agent"] --> N1["노드 1<br/>agent-xyz"]
    DS --> N2["노드 2<br/>agent-abc"]
    DS --> N3["노드 3<br/>agent-def"]
    N4["노드 4 (신규 추가)"] -. 자동 배치 .-> DS

전형적 용도는 노드 수준 인프라:

  • 로깅 에이전트(Fluent Bit, Promtail) — 각 노드의 컨테이너 로그를 수집.
  • 모니터링 에이전트(Node Exporter, cAdvisor) — 노드 메트릭 수집.
  • CNI 데이터플레인(kube-proxy의 일부, Calico/Cilium 에이전트).
  • 스토리지 데몬.

이들은 노드마다 있어야 의미가 있다 — 한 노드의 로그는 그 노드의 에이전트가 읽어야 효율적이다. 그래서 "replicas 수"가 아니라 "노드 수"가 복제본 수를 결정한다.

# Kubernetes 1.36
apiVersion: apps/v1
kind: DaemonSet
metadata: {name: log-agent}
spec:
  selector: {matchLabels: {app: log-agent}}
  template:
    metadata: {labels: {app: log-agent}}
    spec:
      nodeSelector: {disktype: ssd}   # 조건: ssd 노드에만
      containers: [{name: fluentbit, image: fluent/fluent-bit:3.2}]

컨트롤 플레인 노드에도 띄우려면 별도 처리가 필요하다 — 컨트롤 플레인엔 기본 NoSchedule taint가 있어 일반 Pod가 안 스케줄되기 때문. tolerations로 그 taint를 견디게 해야 한다(08-scheduling에서 taint/toleration 재회).

Job — 끝나는 것이 목적인 Pod

Job성공적으로 종료( exit 0)하는 것*이 목적인 Pod를 만든다. Deployment와 근본적으로 다른 점: Job의 Pod는 restartPolicy: OnFailure 또는 Never다. 끝나면 재시작하지 않고(성공 시), *완료(complete) 상태로 남는다.

전형 용도:

  • 일회성 데이터 마이그레이션.
  • 배치 처리(하룻동안 돌 보고서 생성).
  • 데이터 변환 파이프라인.
apiVersion: batch/v1
kind: Job
metadata: {name: db-migrate}
spec:
  completions: 1            # 성공 1회로 완료
  parallelism: 1            # 동시 몇 개
  backoffLimit: 4           # 실패 시 재시도 최대
  template:
    spec:
      restartPolicy: OnFailure
      containers: [{name: migrate, image: myapp/migrate:1.0}]

Job의 핵심 설정 두 개:

  • completions: 성공을 몇 번 해야 Job이 완료로 간주되는가. 1이면 한 번 성공으로 끝.
  • parallelism: 동시에 몇 개의 Pod를 돌릴까. completions: 10, parallelism: 5면 5개씩 묶어 10번 성공까지.

backoffLimit은 실패(비0 종료) 시 재시도 상한. 이 상한을 넘으면 Job은 Failed로 표시된다.

# Kubernetes 1.36 — Job 실행과 완료 상태
kubectl create job pi --image=perl:5.40 -- perl -Mbignum=bpi -wle 'print bpi(50)'
kubectl get job pi

확인할 것: COMPLETIONS 1/1이면 완료. Pod는 Completed 상태로 남는다(삭제되지 않음 — 로그 확인용).

NAME   COMPLETIONS   DURATION   AGE
pi     1/1           8s         12s

CronJob — 주기적으로 Job을 만드는 Job

CronJob은 cron 형식의 스케줄에 따라 Job을 주기적으로 만든다. Job이 실제 작업 Pod를 만들 듯, CronJob은 Job을 만든다 — 한 겹 더 위.

apiVersion: batch/v1
kind: CronJob
metadata: {name: nightly-backup}
spec:
  schedule: "0 2 * * *"          # 매일 새벽 2시
  concurrencyPolicy: Forbid      # 이전 게 안 끝났으면 새 거 안 돌림
  successfulJobsHistoryLimit: 3  # 성공 Job 보존 수
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers: [{name: backup, image: myapp/backup:1.0}]

concurrencyPolicy가 중요하다:

  • Allow(기본): 이전 실행이 아직 돌고 있어도 새 실행을 또 띄운다. 동시에 여러 백업이 돌면 안 되면 곤란.
  • Forbid: 이전 실행이 살아 있으면 새 건너뜀.
  • Replace: 이전 걸 죽이고 새 걸 띄움.

CronJob은 이름 그대로 노드의 cron을 대체하지 않는다 — 클러스터 수준*에서 *Pod로 실행한다는 게 다르다. 그래서 노드 한 대가 죽어도 다른 노드에서 스케줄된다. 단, CronJob 컨트롤러 자체가 컨트롤 플레인에 있으므로 컨트롤 플레인이 죽으면 스케줄도 멈춘다.

세 워크로트 컨트롤러 비교 — 무엇을 쓸 것인가

Deployment StatefulSet DaemonSet Job
복제본 수 결정 replicas (사용자) replicas (사용자) 노드 수 (자동) completions (성공 횟수)
Pod 정체성 무작위·휘발성 안정(번호) 노드당 1개 일회성
Pod 생명 영원(restartPolicy: Always) 영원 영원 종료 지향
스토리지 공통/휘발성 Pod별 전용(고정) 노드 로컬 흔히 작업용 임시
전형 용도 무상태 웹/API DB, 분산 코디네이터 로깅/모니터링/CNI 배치, 마이그레이션

선택은 "이 워크로드가 무엇인가"에서 시작한다: 영원히 돌아야 하는가(아래 3개), 끝나야 하는가(Job). 영원하다면 — 상태가 필요한가(StatefulSet), 노드마다여야 하는가(DaemonSet), 둘 다 아니면(Deployment).

직접 확인하기

# Kubernetes 1.36 — DaemonSet: kind 클러스터에 모든 노드에 하나씩
kubectl create daemonset logger --image=busybox:1.36 -- \
  sh -c 'while true; do echo node-log; sleep 60; done'
kubectl get pods -l app=logger -o wide

확인할 것: 노드마다 Pod가 정확히 하나. (kind는 기본 컨트롤 플레인 1 + 워커.)

kubectl create daemonset은 구버전에서 제한적일 수 있음. 안 되면 YAML apply. 미검증 시 노트.

# Job 완료 상태
kubectl get jobs
# CronJob이 만든 Job들
kubectl create cronjob hello --image=busybox:1.36 --schedule="*/1 * * * *" -- echo hi
kubectl get cronjob,jobs

확인할 것: CronJob이 매분 Job을 만들고, 각 Job이 Pod를 만들어 echo hi 후 Completed.

Job의 podFailurePolicy와 backoffLimit — 실패를 어떻게 다룰까 (1.31+)

Job이 실패를 다루는 방식이 진화했다. 과거엔 backoffLimit(재시도 상한)만 있어 "모든 실패를 같이 취급". podFailurePolicy(1.31 GA)가 실패를 분류:

spec:
  podFailurePolicy:
    rules:
    - action: FailJob              # 즉시 Job 실패
      onExitCodes: {containerName: main, operator: In, values: [42]}  # exit 42는 영구 실패
    - action: Ignore               # 재시도 계속
      onPodConditions: [{type: DisruptionTarget}]   # 노드 장애는 무시

의미: exit code 42(영구적 오류)는 재시도 안 하고 즉시 Job FailJob. 노드 장애(DisruptionTarget)는 무시하고 재시도. 이것이 "일시적 실패는 재시도, 영구 실패는 빨리 포기"를 Job 수준에서 표현. 과거엔 이 분류가 불가능해 무조건 backoffLimit까지 재시도.

completions과 parallelism의 정확한 상호작용

completions(성공 몇 회)과 parallelism(동시 몇 개)의 조합이 Job의 형태를 결정:

completions parallelism Job 형태
1 1 단일 Pod 한 번 성공 (일회성 작업)
N(>1) 1 순차 N번 성공
N M(>1) M개씩 병렬로 N번 성공 (배치)
생략(1) M M개 병렬, 하나라도 성공하면 완료 (경쟁 패턴)
1 생략(1) 가장 단순
# Kubernetes 1.36 — 병렬 Job 예
kubectl create job parallel-demo --image=busybox:1.36 -- \
  sh -c 'echo 작업 $RANDOM' \
  --completions=5 --parallelism=2

확인할 것: 5번 성공까지, 2개씩 동시 Pod. completions과 parallelism이 독립 축 — 총 성공 횟수(completions)와 동시성(parallelism)을 따로 제어.

DaemonSet의 updateStrategy — 노드별 교체

DaemonSet도 롤아웃을 한다. 노드별로 Pod를 교체하는 전략:

  • RollingUpdate(기본): 한 노드씩(또는 여러) 새 버전으로. maxUnavailable으로 동시 교체 노드 수.
  • OnDelete: 수동으로 Pod를 삭제할 때만 새 버전으로. 운영자가 통제.

DaemonSet은 노드마다 1개*라 Deployment와 다른 제약 — maxSurge가 *없다(각 노드에 정확히 1개여야 하므로 초과 생성 불가). 대신 maxUnavailable으로 "몇 노드까지 동시에 Pod 없이 허용"을 제어.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"DaemonSet은 모든 노드의 Deployment다" Deployment는 노드 수와 무관. DaemonSet은 노드당 정확히 1
"Job Pod는 실패해도 영원히 재시작한다" backoffLimit이 있어 영원히 아님. 상한 넘으면 Failed
"CronJob은 노드 cron과 같다" 클러스터 수준 Pod 실행. 노드 죽어도 다른 곳서 스케줄. 단 컨트롤플레인 의존
"DaemonSet은 replicas가 없으니 수동 확장" replicas 개념 자체가 없음. 노드 추가=자동 확장
"Job의 completions과 parallelism은 같다" completions=성공 횟수, parallelism=동시 수. 서로 다른 축
"concurrencyPolicy 기본값이 Forbid다" 기본은 Allow. 겹침을 막으려면 명시해야

요약 — 이 글의 결론

  • DaemonSet은 노드 수가 복제본 수. 로깅·모니터링·CNI 데이터플레인 같은 노드 단위 인프라. 노드 추가=자동 배치, 제거=자동 정리.
  • Job종료 지향 워크로드. completions(성공 몇 번)·parallelism(동시 몇 개)·backoffLimit(재시도 상한)로 배치 작업을 표현. Deployment는 영원히 사는 Pod만 다루므로 못 표현.
  • CronJob은 Job을 주기적으로 만드는 한 겹 위. concurrencyPolicy(Forbid/Replace)로 겹침 제어. 노드 cron이 아니라 클러스터 수준.
  • 선택은 워크로드 성격에서: 영원 vs 종료, 상태 필요 vs 무상태, 노드 단위 vs 클러스터 단위. 이 세 질문이 컨트롤러를 고른다.
  • 컨트롤 플레인 의존성: CronJob/Job 컨트롤러가 컨트롤 플레인에 있어, 그것이 죽으면 배치 스케줄도 멈춘다 — 무상태 워크로드(Deployment Pod)와 다른 취약점.

생각해 볼 문제

  1. DaemonSet을 쓰면 안 되는 상황을 하나 설계하라. (예: 특정 노드 3대에만 2개씩)
  2. completions: 10, parallelism: 2인 Job은 총 몇 개의 Pod를 만드나? 최대 동시 Pod는?
  3. CronJob의 concurrencyPolicy: Allow가 일으킬 수 있는 실무 사고를 시나리오로 그려라.
  4. DaemonSet이 컨트롤 플레인 노드에 뜨지 않는 이유는? 어떻게 띄울 수 있나?
  5. Job Pod가 영원히 멈추지 않는(hang)다면 backoffLimit이 발동할까? 이를 막기 위한 설계는? (job.spec.activeDeadlineSeconds)
  6. 로깅 에이전트를 Deployment로 띄우면 생기는 구체적 문제를 노드 로그 파일 경로(/var/log) 관점에서 설명하라.

참고

'Tech Artifacts > Kubernetes' 카테고리의 다른 글

Kubernetes - 09. storage  (0) 2026.07.13
Kubernetes - 08. scheduling  (0) 2026.07.13
Kubernetes - 06. statefulset  (0) 2026.07.13
Kubernetes - 05. deployment  (0) 2026.07.13
Kubernetes - 04. pod  (0) 2026.07.13