노드마다 로그 수집 에이전트를 띄워야 하고, 동시에 데이터베이스 파드는 순서와 고정 이름을 지켜야 합니다. Deployment 하나로 모든 워크로드를 처리하려 하면 운영 요구사항이 어긋납니다. DaemonSet과 StatefulSet은 특수한 배치 패턴과 상태 있는 서비스를 다루는 도구입니다.
쿠버네티스 클러스터에 수십 개의 노드가 있습니다. 각 노드에서 발생하는 로그를 Elasticsearch로 전송해야 하는데, Deployment로 배포하면 파드 수와 노드 수가 맞지 않아 일부 노드의 로그가 누락됩니다. 신규 노드가 추가될 때마다 수동으로 파드를 배포하는 것도 현실적이지 않습니다. 이런 상황에서 DaemonSet이 등장합니다.
한편, 3개 노드 MySQL 클러스터를 운영 중인데 Deployment로 배포하면 파드가 재시작될 때마다 이름과 IP가 바뀌어 레플리케이션 설정이 깨집니다. 스토리지도 파드마다 분리되어 있어야 하는데, Deployment는 이를 보장하지 않습니다. 이럴 때 StatefulSet이 필요합니다. Deployment가 "누가 처리하든 상관없는 무상태 서비스"를 위한 것이라면, StatefulSet은 "누가, 어떤 순서로, 어떤 데이터를 처리하는지가 중요한 유상태 서비스"를 위한 것입니다.
- 1DaemonSet: 모든 노드에 1개씩 배포 (로그·모니터링 에이전트)
- 2DaemonSet nodeSelector로 특정 노드만 선택
- 3StatefulSet: 순서 보장, 안정적 네트워크 ID
- 4Headless Service와 DNS를 통한 파드 직접 주소 지정
- 5volumeClaimTemplates로 파드별 독립 스토리지
- 6StatefulSet 스케일링 및 업데이트 전략
kubectl get nodes -o widekubectl create namespace ds-sts-demokubectl get storageclasskubectl get nodes --show-labels | head -5DaemonSet: 인프라 에이전트의 표준 배포 방식
신규 노드를 클러스터에 추가했는데 그 노드의 로그가 Elasticsearch에 수집되지 않는다면, 로그 수집 에이전트가 Deployment로 배포되어 있기 때문일 가능성이 높습니다. Deployment는 파드 수를 지정하지 노드 수를 따라가지 않습니다. DaemonSet은 이 문제를 해결하기 위해 "클러스터의 모든 노드에 정확히 1개"를 보장하는 방식으로 동작합니다. 노드가 추가되면 자동으로 파드를 생성하고, 노드가 제거되면 파드도 함께 삭제되기 때문에 로그 수집, 모니터링 에이전트, 네트워크 플러그인처럼 노드 단위로 실행해야 하는 인프라 컴포넌트의 표준 배포 방식입니다.

# fluentd-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
tolerations:
# 마스터 노드(control-plane)에도 배포하려면 필요
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
operator: Exists
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1.16-debian-elasticsearch8-1
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
resources:
requests:
cpu: "100m"
memory: "200Mi"
limits:
cpu: "500m"
memory: "500Mi"
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log # 노드의 로그 디렉토리를 직접 마운트
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
kubectl apply -f fluentd-daemonset.yaml
# 각 노드에 1개씩 파드가 배포됐는지 확인
kubectl get pods -n kube-system -l app=fluentd -o wide
# NAME READY STATUS NODE
# fluentd-7k2p9 1/1 Running worker-1
# fluentd-9m3q7 1/1 Running worker-2
# fluentd-xn8tv 1/1 Running worker-3
# DaemonSet 상태 요약
kubectl get daemonset fluentd -n kube-system
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
# fluentd 3 3 3 3 3 <none> 2m
# 특정 노드에만 배포 (nodeSelector 사용)
# 예: ssd=true 레이블이 있는 노드에만
kubectl label node worker-1 ssd=true
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
DaemonSet에 nodeSelector: {ssd: "true"}를 추가하면 레이블이 있는 노드에만 배포됩니다.
StatefulSet: 순서와 ID가 보장되는 파드
MySQL 레플리케이션 구성에서 슬레이브가 "어느 마스터에 연결할지" 알아야 하는데, Deployment로 배포하면 파드 이름과 IP가 재시작마다 바뀌어 레플리케이션 설정이 깨집니다. 파드 재시작 후에도 동일한 이름(mysql-0, mysql-1)과 동일한 스토리지를 유지하는 것이 StatefulSet의 핵심입니다. 순서 보장은 단순 편의가 아니라 마스터가 준비되기 전에 슬레이브를 시작하는 실수를 방지하는 운영 안전장치입니다. 데이터베이스, 메시지 큐, 분산 조율 서비스처럼 "어느 인스턴스인지"가 의미를 갖는 유상태 서비스에 StatefulSet이 필요합니다.

- 순서 보장: web-0 → web-1 → web-2 순서로 생성, 역순으로 삭제
- 안정적 네트워크 ID: 재시작해도 동일한 이름 유지 (
web-0,web-1, ...) - 안정적 스토리지: 파드가 재스케줄돼도 동일한 PVC 재연결
# mysql-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: ds-sts-demo
labels:
app: mysql
spec:
ports:
- port: 3306
clusterIP: None # ← Headless Service (클러스터 IP 없음)
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: ds-sts-demo
spec:
serviceName: "mysql" # Headless Service 이름과 일치해야 함
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
volumeClaimTemplates: # ← 파드별 독립 PVC 자동 생성
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "standard"
resources:
requests:
storage: 10Gi
kubectl apply -f mysql-statefulset.yaml
# 파드 생성 순서 관찰 (0 → 1 → 2 순서로)
kubectl get pods -n ds-sts-demo -w
# NAME READY STATUS AGE
# mysql-0 0/1 Pending 0s
# mysql-0 0/1 Running 3s
# mysql-0 1/1 Running 10s ← 0번이 Ready된 후에야
# mysql-1 0/1 Pending 11s ← 1번 생성 시작
# mysql-1 1/1 Running 25s
# mysql-2 0/1 Pending 26s
# mysql-2 1/1 Running 40s
# 각 파드별 독립 PVC 생성 확인
kubectl get pvc -n ds-sts-demo
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# data-mysql-0 Bound pvc-xxx 10Gi RWO standard
# data-mysql-1 Bound pvc-yyy 10Gi RWO standard
# data-mysql-2 Bound pvc-zzz 10Gi RWO standard
Headless Service와 DNS: 파드 직접 주소 지정
일반 Service는 여러 파드를 하나의 클러스터 IP로 묶어 로드밸런싱합니다. MySQL 클러스터에서 슬레이브가 항상 마스터(mysql-0)에만 연결해야 하는 상황에서 이 로드밸런싱은 오히려 방해가 됩니다. Headless Service는 clusterIP: None으로 설정하면 단일 IP 없이 각 파드의 IP가 DNS에 직접 등록되어 mysql-0.mysql.namespace.svc.cluster.local 형식으로 특정 파드를 직접 주소 지정할 수 있습니다. StatefulSet과 Headless Service를 함께 써야 "순서 보장 + 안정적 네트워크 ID"가 완성됩니다.
일반 Service DNS:
mysql.ds-sts-demo.svc.cluster.local → 단일 ClusterIP (로드밸런싱)
Headless Service DNS:
mysql.ds-sts-demo.svc.cluster.local → 전체 파드 IP 목록
mysql-0.mysql.ds-sts-demo.svc.cluster.local → mysql-0 파드 IP
mysql-1.mysql.ds-sts-demo.svc.cluster.local → mysql-1 파드 IP
mysql-2.mysql.ds-sts-demo.svc.cluster.local → mysql-2 파드 IP
# DNS 확인 (클러스터 내 busybox 파드에서)
kubectl run dns-test --image=busybox:1.36 -it --rm -n ds-sts-demo \
-- nslookup mysql-0.mysql.ds-sts-demo.svc.cluster.local
# Server: 10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
# Name: mysql-0.mysql.ds-sts-demo.svc.cluster.local
# Address 1: 10.244.1.5 mysql-0.mysql.ds-sts-demo.svc.cluster.local
# MySQL 레플리케이션 설정 예시 (mysql-1에서 실행)
kubectl exec mysql-1 -n ds-sts-demo -- mysql -uroot -p$MYSQL_ROOT_PASSWORD -e \
"CHANGE MASTER TO MASTER_HOST='mysql-0.mysql.ds-sts-demo.svc.cluster.local', MASTER_PORT=3306;"
실습: StatefulSet 스케일링과 업데이트
# StatefulSet 스케일 업 (3 → 5)
kubectl scale statefulset mysql -n ds-sts-demo --replicas=5
# 스케일 다운 (5 → 3, 역순 삭제)
kubectl scale statefulset mysql -n ds-sts-demo --replicas=3
# mysql-4 → mysql-3 순서로 삭제
# 롤링 업데이트 (partition을 이용한 카나리 방식)
# partition=2 이면 mysql-2, mysql-1, mysql-0은 업데이트 안 됨
kubectl patch statefulset mysql -n ds-sts-demo \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
# 이미지 업데이트 (mysql-2만 업데이트됨)
kubectl set image statefulset/mysql mysql=mysql:8.0.36 -n ds-sts-demo
# 검증 후 partition을 0으로 변경해 전체 업데이트
kubectl patch statefulset mysql -n ds-sts-demo \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
StatefulSet을 삭제하고 재생성했는데 파드가 Pending에서 멈춥니다. 또는 StorageClass가 없는 환경에서 StatefulSet을 배포하면 동일한 증상이 발생합니다.
스토리지 리소스 삭제
안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc data-mysql-0 data-mysql-1 data-mysql-2 -n ds-sts-demo위 항목을 모두 확인한 후 복사할 수 있습니다
# 증상 확인
kubectl get pods -n ds-sts-demo
# NAME READY STATUS RESTARTS AGE
# mysql-0 0/1 Pending 0 5m
# (mysql-1, mysql-2는 생성되지 않음 — 순서 보장 때문)
# 1단계: 파드 이벤트 확인
kubectl describe pod mysql-0 -n ds-sts-demo | grep -A 10 "Events"
# Events:
# Warning FailedScheduling 3m pod/mysql-0
# 0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims
# 2단계: PVC 상태 확인
kubectl get pvc -n ds-sts-demo
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# data-mysql-0 Pending standard 5m
# 3단계: PVC 상세 이벤트 확인
kubectl describe pvc data-mysql-0 -n ds-sts-demo | grep -A 10 "Events"
# Events:
# Warning ProvisioningFailed 3m
# storageclass.storage.k8s.io "standard" not found
# 4단계: 사용 가능한 StorageClass 확인
kubectl get storageclass
# (아무 출력도 없음 — StorageClass 없음)
# 또는
# NAME PROVISIONER AGE
# local-path (default) rancher.io/local-path 10d
# 5단계-A: StorageClass 이름 수정
# volumeClaimTemplates의 storageClassName을 실제 존재하는 이름으로 변경
kubectl patch statefulset mysql -n ds-sts-demo \
--type='json' \
-p='[{"op":"replace","path":"/spec/volumeClaimTemplates/0/spec/storageClassName","value":"local-path"}]'
# 5단계-B: 이미 PVC가 생성된 경우 (재생성 케이스)
# StatefulSet 삭제 시 PVC는 자동 삭제되지 않음 (데이터 보호)
# 남아있는 PVC를 확인하고 필요 시 수동 삭제
kubectl get pvc -n ds-sts-demo
kubectl delete pvc data-mysql-0 data-mysql-1 data-mysql-2 -n ds-sts-demo
# StatefulSet 재생성
kubectl apply -f mysql-statefulset.yaml
# 정상 동작 확인
kubectl get pvc -n ds-sts-demo
# NAME STATUS VOLUME CAPACITY
# data-mysql-0 Bound pvc-xxx 10Gi ← Bound 상태
중요 주의사항: StatefulSet을 삭제해도 PVC는 삭제되지 않습니다. 이는 의도적인 설계로 데이터를 보호합니다. 새 StatefulSet을 배포하면 기존 PVC를 재사용합니다. 완전한 초기화가 필요하면 PVC를 수동으로 삭제해야 하며, 이 작업은 데이터 영구 삭제를 의미합니다.
시나리오: 프로덕션 클러스터에 Prometheus 모니터링 스택 배포
새 쿠버네티스 클러스터를 구축하고 모니터링을 설정해야 합니다. node-exporter는 DaemonSet으로, Prometheus는 StatefulSet으로 배포합니다.
# node-exporter DaemonSet (각 노드 메트릭 수집)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true # 노드 네트워크 인터페이스 접근
hostPID: true # 노드 프로세스 접근
tolerations:
- operator: Exists # 모든 taint 허용 (마스터 포함)
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
ports:
- containerPort: 9100
hostPort: 9100
resources:
requests:
cpu: "50m"
memory: "30Mi"
limits:
cpu: "200m"
memory: "100Mi"
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
EOF
# Prometheus StatefulSet (시계열 데이터 영구 저장)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: prometheus
namespace: monitoring
spec:
serviceName: "prometheus"
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.51.0
args:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
ports:
- containerPort: 9090
volumeMounts:
- name: config
mountPath: /etc/prometheus
- name: data
mountPath: /prometheus
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumes:
- name: config
configMap:
name: prometheus-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "standard"
resources:
requests:
storage: 50Gi
EOF
# 배포 확인
kubectl get daemonset node-exporter -n monitoring
# DESIRED = 클러스터 노드 수여야 함
kubectl get statefulset prometheus -n monitoring
kubectl get pvc -n monitoring
실무 포인트: node-exporter는 DaemonSet으로 배포해야 노드가 추가될 때 자동으로 메트릭 수집이 시작됩니다. Prometheus는 StatefulSet으로 배포해야 파드가 재시작되어도 data-prometheus-0 PVC에 저장된 30일치 시계열 데이터가 보존됩니다.
핵심 요약
| 항목 | Deployment | DaemonSet | StatefulSet |
|---|---|---|---|
| 파드 수 | replicas 지정 | 노드당 1개 | replicas 지정 |
| 파드 이름 | 랜덤 suffix | 랜덤 suffix | web-0, web-1 (고정) |
| 생성/삭제 순서 | 무작위 | 무작위 | 순서 보장 |
| 스토리지 | 공유 or 없음 | hostPath 주로 | 파드별 독립 PVC |
| DNS | Service통해 | Service통해 | 파드별 직접 주소 |
| 사용 사례 | 웹 서버, API | 로그, 모니터링 에이전트 | DB, 메시지 큐, Zookeeper |