"모든 노드에 무조건 하나", "끝나고 죽는 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}]
컨트롤 플레인 노드에도 띄우려면 별도 처리가 필요하다 — 컨트롤 플레인엔 기본
NoScheduletaint가 있어 일반 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)와 다른 취약점.
생각해 볼 문제
- DaemonSet을 쓰면 안 되는 상황을 하나 설계하라. (예: 특정 노드 3대에만 2개씩)
completions: 10, parallelism: 2인 Job은 총 몇 개의 Pod를 만드나? 최대 동시 Pod는?- CronJob의
concurrencyPolicy: Allow가 일으킬 수 있는 실무 사고를 시나리오로 그려라. - DaemonSet이 컨트롤 플레인 노드에 뜨지 않는 이유는? 어떻게 띄울 수 있나?
- Job Pod가 영원히 멈추지 않는(hang)다면 backoffLimit이 발동할까? 이를 막기 위한 설계는? (job.spec.activeDeadlineSeconds)
- 로깅 에이전트를 Deployment로 띄우면 생기는 구체적 문제를 노드 로그 파일 경로(
/var/log) 관점에서 설명하라.
참고
- Kubernetes 공식 문서 - DaemonSet - 접근 2026-07-12
- Kubernetes 공식 문서 - Jobs - 접근 2026-07-12 (completions/parallelism/backoffLimit)
- Kubernetes 공식 문서 - CronJobs - 접근 2026-07-12 (concurrencyPolicy, schedule)
- Kubernetes 공식 문서 - Pod의 restartPolicy와 Job - 접근 2026-07-12
- Kubernetes 1.36 API - DaemonSet/Job/CronJob specs - 접근 2026-07-12
'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 |
