StatefulSet과 DaemonSet — Deployment만으로 안 되는 워크로드 유형
Deployment로 대부분의 워크로드를 배포할 수 있는데, 왜 StatefulSet이나 DaemonSet 같은 별도의 컨트롤러가 필요할까요?
Deployment는 무상태(Stateless) 애플리케이션에 최적화되어 있습니다. Pod 이름이 랜덤하고, 어떤 순서로 생성/삭제되든 상관없으며, 볼륨도 공유합니다. 하지만 데이터베이스처럼 각 인스턴스가 고유한 ID와 전용 스토리지를 가져야 하거나, 모든 노드에 에이전트를 배포해야 하는 경우에는 다른 컨트롤러가 필요합니다.
StatefulSet — 상태를 가진 워크로드
StatefulSet은 다음 세 가지를 보장합니다.
- 고유하고 안정적인 네트워크 ID: Pod 이름이 순번으로 결정됩니다 (예:
mysql-0,mysql-1) - 순서 보장: 생성은 0번부터, 삭제는 역순으로 진행됩니다
- 개별 퍼시스턴트 스토리지: 각 Pod에 전용 PVC가 할당됩니다
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # Headless Service 필수
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates: # 각 Pod에 개별 PVC 생성
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 10Gi
Headless Service와의 조합
StatefulSet은 반드시 Headless Service가 필요합니다. 이를 통해 각 Pod에 고유한 DNS 레코드가 생성됩니다.
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
# 각 Pod의 DNS
# mysql-0.mysql-headless.default.svc.cluster.local
# mysql-1.mysql-headless.default.svc.cluster.local
# mysql-2.mysql-headless.default.svc.cluster.local
Primary-Replica 구성에서 쓰기는 mysql-0에, 읽기는 다른 Pod에 보내는 식으로 활용할 수 있습니다.
Pod 관리 정책
spec:
podManagementPolicy: OrderedReady # 기본값: 순차 생성/삭제
# podManagementPolicy: Parallel # 병렬 생성/삭제 (순서 불필요 시)
OrderedReady는 mysql-0이 Ready가 되어야 mysql-1이 생성됩니다. 순서가 중요하지 않은 경우 Parallel로 배포 속도를 높일 수 있습니다.
업데이트 전략
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 1 # 1번 이상의 Pod만 업데이트 (카나리 패턴)
partition을 활용하면 특정 번호 이상의 Pod만 업데이트할 수 있어 단계적 롤아웃이 가능합니다.
DaemonSet — 모든 노드에 하나씩
DaemonSet은 클러스터의 모든 노드에 정확히 하나의 Pod을 실행합니다. 노드가 추가되면 자동으로 Pod이 생성되고, 노드가 제거되면 Pod도 삭제됩니다.
대표적인 사용 사례
- 로그 수집: Fluentd, Fluent Bit
- 모니터링 에이전트: Node Exporter, Datadog Agent
- 네트워크 플러그인: Calico, Cilium
- 스토리지 드라이버: CSI Node Driver
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:2.1
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: containers
mountPath: /var/lib/docker/containers
readOnly: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
tolerations:
# 마스터 노드에서도 실행
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
volumes:
- name: varlog
hostPath:
path: /var/log
- name: containers
hostPath:
path: /var/lib/docker/containers
특정 노드에만 배포
spec:
template:
spec:
nodeSelector:
disk-type: ssd # SSD 노드에만 배포
# 또는 affinity 사용
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role
operator: In
values: ["worker"]
업데이트 전략
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 한 번에 1개 노드씩 업데이트
OnDelete 전략을 사용하면 수동으로 Pod을 삭제할 때만 새 버전이 배포됩니다.
Job — 한 번 실행하고 종료
Job은 완료될 때까지 Pod을 실행하고, 성공하면 종료합니다.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
completions: 1 # 성공해야 하는 횟수
parallelism: 1 # 동시 실행 Pod 수
backoffLimit: 3 # 실패 시 재시도 횟수
activeDeadlineSeconds: 300 # 최대 실행 시간 (5분)
template:
spec:
restartPolicy: Never # Job에서는 Never 또는 OnFailure
containers:
- name: migrate
image: my-app:1.0
command: ["./migrate", "--up"]
병렬 처리 Job
spec:
completions: 10 # 10개 작업을 모두 완료해야 함
parallelism: 3 # 동시에 3개 Pod 실행
# Job 상태 확인
kubectl get jobs db-migration
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 45s 2m
CronJob — 스케줄 기반 반복 실행
CronJob은 cron 표현식에 따라 주기적으로 Job을 생성합니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-backup
spec:
schedule: "0 2 * * *" # 매일 새벽 2시
concurrencyPolicy: Forbid # 이전 Job이 실행 중이면 새 Job 생성 안 함
successfulJobsHistoryLimit: 3 # 성공 이력 보관 수
failedJobsHistoryLimit: 1 # 실패 이력 보관 수
startingDeadlineSeconds: 200 # 스케줄 시간 이후 200초 내에 시작 안 되면 스킵
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: backup-tool:1.0
command: ["/bin/sh", "-c", "pg_dump mydb > /backup/dump.sql"]
concurrencyPolicy 옵션
| 값 | 설명 |
|---|---|
Allow | 동시 실행 허용 (기본값) |
Forbid | 이전 Job이 실행 중이면 새 Job 스킵 |
Replace | 이전 Job을 종료하고 새 Job 실행 |
Deployment vs StatefulSet vs DaemonSet 비교
| 특성 | Deployment | StatefulSet | DaemonSet |
|---|---|---|---|
| Pod 이름 | 랜덤 해시 | 순번 (0, 1, 2...) | 노드별 1개 |
| 스토리지 | 공유 가능 | 개별 PVC | 보통 hostPath |
| 순서 보장 | 없음 | 있음 | 없음 |
| 스케일링 | replicas 조절 | replicas 조절 | 노드 수에 연동 |
| 사용 사례 | 웹 서버, API | DB, 캐시 | 에이전트, 모니터링 |
실무에서 주의할 점
- StatefulSet Pod 삭제 시 PVC는 남습니다: 데이터 보존을 위해 의도적인 설계이지만, 정리를 잊으면 불필요한 볼륨 비용이 발생합니다
- DaemonSet은 리소스 제한을 꼭 설정하세요: 모든 노드에서 실행되므로 리소스를 과하게 사용하면 전체 클러스터에 영향을 줍니다
- CronJob의 타임존: Kubernetes 1.27부터
.spec.timeZone필드로 타임존을 지정할 수 있습니다
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Seoul" # KST 기준
정리
Deployment는 무상태 워크로드의 기본이지만, 고유 ID와 전용 스토리지가 필요하면 StatefulSet, 모든 노드에 에이전트를 배포하려면 DaemonSet, 한 번 실행하고 끝나는 작업에는 Job/CronJob을 사용합니다. 각 컨트롤러의 특성을 이해하고 워크로드 성격에 맞는 것을 선택하는 것이 중요합니다.