infra
Platform

모듈 맵

[Kubernetes] DaemonSet과 상태 저장형 앱 배포를 위한 StatefulSet 완벽 분석

0 / 29 완료

펼치기
0 / 29 완료0%

Kubernetes · 14 / 29

[Kubernetes] DaemonSet과 상태 저장형 앱 배포를 위한 StatefulSet 완벽 분석

모든 노드에 에이전트를 배포하는 DaemonSet과 순서·상태를 보장하는 StatefulSet을 이해합니다

🚨INCIDENT ALERT
HIGH

노드마다 로그 수집 에이전트를 띄워야 하고, 동시에 데이터베이스 파드는 순서와 고정 이름을 지켜야 합니다. 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 wide
실습용 네임스페이스 생성
kubectl create namespace ds-sts-demo
StorageClass 확인 (StatefulSet 실습 필요)
kubectl get storageclass
노드 레이블 확인
kubectl get nodes --show-labels | head -5
💡개념

DaemonSet: 인프라 에이전트의 표준 배포 방식

신규 노드를 클러스터에 추가했는데 그 노드의 로그가 Elasticsearch에 수집되지 않는다면, 로그 수집 에이전트가 Deployment로 배포되어 있기 때문일 가능성이 높습니다. Deployment는 파드 수를 지정하지 노드 수를 따라가지 않습니다. DaemonSet은 이 문제를 해결하기 위해 "클러스터의 모든 노드에 정확히 1개"를 보장하는 방식으로 동작합니다. 노드가 추가되면 자동으로 파드를 생성하고, 노드가 제거되면 파드도 함께 삭제되기 때문에 로그 수집, 모니터링 에이전트, 네트워크 플러그인처럼 노드 단위로 실행해야 하는 인프라 컴포넌트의 표준 배포 방식입니다.

DaemonSet: 인프라 에이전트의 표준 배포 방식

YAML
# 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
Kubernetes
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/READYRunning, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
  • RESTARTS/EVENTS재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.

DaemonSet에 nodeSelector: {ssd: "true"}를 추가하면 레이블이 있는 노드에만 배포됩니다.

💡개념

StatefulSet: 순서와 ID가 보장되는 파드

MySQL 레플리케이션 구성에서 슬레이브가 "어느 마스터에 연결할지" 알아야 하는데, Deployment로 배포하면 파드 이름과 IP가 재시작마다 바뀌어 레플리케이션 설정이 깨집니다. 파드 재시작 후에도 동일한 이름(mysql-0, mysql-1)과 동일한 스토리지를 유지하는 것이 StatefulSet의 핵심입니다. 순서 보장은 단순 편의가 아니라 마스터가 준비되기 전에 슬레이브를 시작하는 실수를 방지하는 운영 안전장치입니다. 데이터베이스, 메시지 큐, 분산 조율 서비스처럼 "어느 인스턴스인지"가 의미를 갖는 유상태 서비스에 StatefulSet이 필요합니다.

StatefulSet: 순서와 ID가 보장되는 파드

  1. 순서 보장: web-0 → web-1 → web-2 순서로 생성, 역순으로 삭제
  2. 안정적 네트워크 ID: 재시작해도 동일한 이름 유지 (web-0, web-1, ...)
  3. 안정적 스토리지: 파드가 재스케줄돼도 동일한 PVC 재연결
YAML
# 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
Kubernetes
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
Kubernetes
# 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 스케일링과 업데이트

Kubernetes
# 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을 배포하면 동일한 증상이 발생합니다.

위험 명령어PVC/PV 삭제는 reclaimPolicy와 스토리지 설정에 따라 실제 데이터 손실로 이어질 수 있습니다.

스토리지 리소스 삭제

안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc data-mysql-0 data-mysql-1 data-mysql-2 -n ds-sts-demo

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# 증상 확인
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일치 시계열 데이터가 보존됩니다.

핵심 요약

항목DeploymentDaemonSetStatefulSet
파드 수replicas 지정노드당 1개replicas 지정
파드 이름랜덤 suffix랜덤 suffixweb-0, web-1 (고정)
생성/삭제 순서무작위무작위순서 보장
스토리지공유 or 없음hostPath 주로파드별 독립 PVC
DNSService통해Service통해파드별 직접 주소
사용 사례웹 서버, API로그, 모니터링 에이전트DB, 메시지 큐, Zookeeper

지식 확인

퀴즈 — 3문제

Q1

DaemonSet이 Deployment와 다른 핵심 특성은?

Q2

StatefulSet에서 파드 이름이 web-0, web-1, web-2처럼 고정 인덱스로 생성되는 이유는?

Q3

StatefulSet의 파드가 Pending 상태에서 멈추고 이벤트에 'no persistent volumes available for this claim'이 표시될 때 원인은?

0 / 3 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

kubernetes고급 · 55
[Kubernetes] ServiceAccount를 이용한 컨테이너 내부의 API 서버 안전 통신
Kubernetes 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점