infra
Platform

모듈 맵

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

0 / 29 완료

펼치기
0 / 29 완료0%

쿠버네티스 & GitOps · 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 vs Deployment — Deployment는 지정한 replicas 수만큼 파드를 임의 노드에 배치(노드 수와 무관)하지만, DaemonSet은 모든 노드에 파드를 정확히 하나씩 보장하고 신규 노드 추가 시 자동 배치. 로그 수집(Fluentd)·모니터링 에이전트(node-exporter)·CNI처럼 노드마다 떠야 하는 인프라 컴포넌트에 사용확대

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
🔍실행 후 확인할 것
  • kubectl get daemonset에서 DESIRED와 CURRENT 수치를 먼저 비교 — DESIRED=노드 수, CURRENT<DESIRED이면 일부 노드에 파드가 뜨지 않은 것으로 kubectl describe ds <name>으로 이벤트 확인
  • DESIRED 수치 기준: 클러스터 노드 전체 수와 일치해야 정상 (nodeSelector 적용 시엔 해당 레이블 노드 수와 일치). 0이면 nodeSelector 레이블이 아무 노드에도 없는 것
  • READY < DESIRED이고 STATUS=Running이면 → 파드는 떴지만 readinessProbe 실패 상태. kubectl describe pod -n kube-system <ds-pod>에서 Conditions의 Ready=False 항목과 Events 확인

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

💡개념

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

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

StatefulSet 순서와 안정적 ID — Deployment와 달리 파드에 안정적 이름(mysql-0·mysql-1)·순서(0→1→2 순차 생성, 역순 삭제)·고정 스토리지(PVC)·안정적 네트워크 ID를 보장. 재시작·재스케줄 후에도 이름이 유지돼 DB 레플리케이션·클러스터처럼 멤버 신원이 중요한 스테이트풀 워크로드에 사용확대

  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
💡개념

StatefulSet이 파드를 0번부터 순서대로 세우는 법 — 생성부터 삭제까지 5단계

kubectl applyreplicas: 3 StatefulSet을 올리면 파드가 한꺼번에 뜨지 않고 mysql-0mysql-1mysql-2 순서로 하나씩 올라옵니다. Deployment라면 3개가 병렬로 뜨는데 StatefulSet은 왜 줄을 세울까요? MySQL 슬레이브가 마스터(mysql-0)에 붙어 복제를 시작하려면 마스터가 먼저 준비돼 있어야 하고, 클러스터형 앱은 먼저 뜬 멤버에 조인해야 하기 때문입니다. 이 순서 보장이 어떻게 동작하는지 알면 "왜 mysql-1이 안 뜨지", "재시작했는데 왜 같은 볼륨에 다시 붙지"를 단계로 좁혀 진단할 수 있습니다.

TEXT
kubectl apply -f statefulset.yaml   (replicas: 3, serviceName: mysql)
   │
   ① ordinal 0부터 순서대로 진행              (mysql-0 → mysql-1 → mysql-2)
   │
   ② 그 파드의 PVC를 먼저 생성·바인딩          (data-mysql-0 → PV, 1:1 고정)
   │
   ③ 파드 스케줄 + Headless로 DNS 등록         (mysql-0.mysql.ns.svc.cluster.local)
   │
   ④ Ready가 될 때까지 대기 후 다음으로         (OrderedReady: mysql-0 Ready 후 mysql-1 시작)
   │    → ②~④를 mysql-1, mysql-2에 대해 반복
   │
   ⑤ 스케일 다운·삭제는 정확히 역순            (mysql-2 → mysql-1 → mysql-0)
   ▼
안정적 이름 + 고정 볼륨 + 순서 = 클러스터 멤버 신원이 재시작 후에도 유지

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 순차 진행컨트롤러가 ordinal 낮은 것부터 하나씩 처리. podManagementPolicy 기본값은 OrderedReady순서가 필요 없는 앱인데 기동이 느리면 → Parallel로 바꿔 병렬 기동
② PVC 고정volumeClaimTemplates가 파드마다 전용 PVC 생성(data-mysql-0). 같은 ordinal은 항상 자기 PVC를 다시 마운트매칭 PV 없음·StorageClass 없음 → PVC가 Pending → 파드도 Pending
③ DNS 등록Headless Service(clusterIP: None)가 mysql-0.mysql... 형태의 안정적 DNS를 부여serviceName이 Headless와 불일치 → 파드별 DNS가 안 생겨 피어를 못 찾음
④ Ready 대기앞 파드가 Ready가 돼야 다음 ordinal 생성 시작(OrderedReady)mysql-0이 Pending·CrashLoop이면 mysql-1 이후는 생성조차 안 됨
⑤ 역순 삭제스케일 다운·삭제 시 높은 ordinal부터 제거. PVC는 남겨 데이터 보호재생성 시 남은 PVC를 그대로 재사용 → 옛 데이터가 다시 붙음(초기화하려면 PVC 수동 삭제)

즉 StatefulSet의 순서 보장은 한 파드가 ②PVC 바인딩 → ③DNS 등록 → ④Ready를 통과해야 다음 파드로 넘어가는 직렬 파이프라인입니다. 그래서 mysql-1이 안 보이면 거의 항상 mysql-0이 아직 Ready가 아닌 것이고(④), 그 원인은 대개 ②(PVC Pending)로 거슬러 올라갑니다 — kubectl get pvckubectl describe pod mysql-0을 순서대로 보면 어느 단계에서 멈췄는지 바로 드러납니다. 반대로 DaemonSet은 이런 순서·ordinal·PVC 개념 없이 "노드마다 1개"를 병렬로 채우는, 정반대의 배치 모델입니다.

💡개념

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 vs Headless Service DNS 동작 비교 — 일반 Service(clusterIP 10.96.0.20)는 mysql.ns.svc 질의에 단일 ClusterIP 하나만 반환하고 kube-proxy가 임의 파드로 로드밸런싱하므로 특정 파드를 고를 수 없다. Headless Service(clusterIP None)는 같은 질의에 전체 파드 IP 목록을, mysql-0.mysql.ns.svc 질의에는 mysql-0 파드 IP를 직접 반환해 이름으로 특정 파드를 지정할 수 있다. StatefulSet과 Headless Service를 함께 써야 슬레이브가 항상 마스터 mysql-0에 고정 연결되어 레플리케이션이 유지된다확대

일반 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를 수동으로 삭제해야 하며, 이 작업은 데이터 영구 삭제를 의미합니다.

심화 — 클러스터형 앱이 부팅에서 멈추는 이유

💡개념

심화: not-Ready 파드의 DNS와 publishNotReadyAddresses

StatefulSet + Headless Service로 mysql-0.mysql... 같은 안정적 DNS를 얻는다고 배웠습니다. 그런데 Cassandra·Zookeeper·RabbitMQ 같은 클러스터형 앱을 처음 띄울 때, 파드들이 서로를 DNS로 못 찾아 부팅이 영영 안 끝나는 상황을 만나게 됩니다. 원인은 언제 DNS 레코드가 게시되느냐에 있습니다.

  • 기본 규칙: Service는 기본적으로 Ready인 파드의 주소만 엔드포인트에 넣고 DNS에 게시합니다. Headless Service도 마찬가지라, 아직 Ready가 아닌 파드의 mysql-1.mysql... 같은 이름은 조회되지 않습니다.
  • 부트스트랩 데드락: 클러스터형 앱은 흔히 readiness를 클러스터 합류 완료로 정의합니다. 그런데 합류하려면 시드(seed) 피어를 DNS로 찾아야 하고, 그 피어도 아직 Ready가 아니라 DNS에 없다면 — 서로가 서로의 Ready를 기다리는 순환이 생겨 전원이 not-Ready에 갇힙니다.
  • 해법 publishNotReadyAddresses: Headless Service에 spec.publishNotReadyAddresses: true를 주면 Ready 여부와 무관하게 파드 DNS가 즉시 게시됩니다. 그래서 부팅 중인 파드끼리도 서로를 찾아 클러스터를 형성할 수 있습니다. 스테이트풀 클러스터용 공식 차트들이 이 옵션을 켜 두는 이유입니다.

주의할 점은 이 옵션이 피어 디스커버리(헤드리스)용이라는 것입니다. 일반 트래픽을 받는 Service까지 not-Ready 주소를 게시하면 준비 안 된 파드로 요청이 가므로, 클라이언트용 Service와 피어 디스커버리용 헤드리스 Service를 분리해 쓰는 것이 정석입니다.

상황: Zookeeper(또는 Cassandra) 같은 클러스터 앱을 StatefulSet + Headless Service로 배포했습니다. 그런데 어느 파드도 Ready가 되지 않고 0/1로 멈추고, 로그를 보면 다른 멤버의 DNS 이름(예: app-1.app-headless.ns.svc.cluster.local)을 resolve 못 한다는 에러가 계속 나옵니다. PVC는 정상 Bound라 스토리지 문제도 아닙니다.

원인: 앱의 readinessProbe가 클러스터 합류 완료를 Ready 조건으로 삼는데, 합류하려면 피어를 DNS로 찾아야 합니다. 그러나 Headless Service는 기본적으로 Ready인 파드만 DNS에 게시하므로, 아직 not-Ready인 피어들의 이름이 서로에게 보이지 않습니다. 전원이 서로의 Ready를 기다리는 순환 데드락에 빠진 것입니다. 기본 OrderedReady 정책이면 app-0이 Ready가 안 돼 app-1조차 생성되지 않아 증상이 더 굳어집니다.

진단: 파드 안에서 nslookup으로 피어 이름(app-1.app-headless.ns.svc.cluster.local)을 조회해 안 나오는지 확인합니다. kubectl get endpoints <headless-svc> -o yaml로 해당 주소가 정상 게시가 아니라 notReadyAddresses에만 있는지 봅니다. Service spec에 publishNotReadyAddresses가 없으면 원인이 확정됩니다.

해결: Headless Service에 publishNotReadyAddresses: true를 추가해 부팅 중 파드도 DNS로 서로를 찾게 합니다. 클러스터형 스테이트풀 앱의 공식 차트는 대개 이 옵션이 켜져 있으니 직접 매니페스트를 쓸 때 빠뜨리지 않습니다. 순차 기동이 부트스트랩을 방해하면 podManagementPolicy: Parallel을 함께 고려하고, 일반 트래픽용 Service는 이 옵션 없이 두어 준비된 파드로만 요청이 가게 분리합니다.

💼
실무 맥락
현업 패턴

시나리오: 프로덕션 클러스터에 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
실습 단계
1

실습 네임스페이스 생성

kubectl create namespace ds-sts-demo

예상 출력

namespace/ds-sts-demo created
2

DaemonSet 배포 및 노드별 배치 확인

kubectl apply -f - <<'EOF' apiVersion: apps/v1 kind: DaemonSet metadata: name: node-logger namespace: ds-sts-demo spec: selector: matchLabels: app: node-logger template: metadata: labels: app: node-logger spec: containers: - name: logger image: busybox command: ["sh", "-c", "while true; do echo node-log; sleep 60; done"] resources: requests: cpu: "10m" memory: "16Mi" limits: cpu: "50m" memory: "32Mi" EOF kubectl get daemonset node-logger -n ds-sts-demo

예상 출력

NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE
node-logger   1         1         1       1            1
3

DaemonSet 파드 노드 배치 확인

kubectl get pods -n ds-sts-demo -l app=node-logger -o wide

예상 출력

NAME                READY   STATUS    NODE
node-logger-xxxxx   1/1     Running   minikube
4

StatefulSet 배포 및 순서 있는 파드 생성 확인

kubectl apply -f - <<'EOF' apiVersion: v1 kind: Service metadata: name: web-headless namespace: ds-sts-demo spec: clusterIP: None selector: app: web ports: - port: 80 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web namespace: ds-sts-demo spec: serviceName: "web-headless" replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:alpine resources: requests: cpu: "10m" memory: "16Mi" limits: cpu: "100m" memory: "64Mi" EOF kubectl get pods -n ds-sts-demo -l app=web

예상 출력

NAME      READY   STATUS    AGE
web-0     1/1     Running   30s
web-1     1/1     Running   20s
web-2     1/1     Running   10s
5

실습 리소스 정리

kubectl delete namespace ds-sts-demo

예상 출력

namespace "ds-sts-demo" deleted

명령어·단축키 빠른 참조

이 모듈에서 DaemonSet·StatefulSet을 배포·스케일·업데이트하고 스토리지/DNS 문제를 진단할 때 쓴 kubectl 명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
kubectl get nodes -o wide노드 목록·IP 확인(DaemonSet 대상)kubectl get nodes --show-labels (레이블까지)
kubectl get daemonsetDaemonSet DESIRED/CURRENT 비교DESIRED=노드 수여야 정상, 불일치 시 describe ds
kubectl label nodenodeSelector 대상 노드 지정kubectl label node worker-1 ssd=true
kubectl get pods -o wide파드가 어느 노드에 떴는지... -l app=fluentd -o wide
kubectl get pods -w생성 순서 실시간 관찰(0→1→2)StatefulSet 순차 기동 확인
kubectl get statefulsetStatefulSet 상태 확인READY 수가 replicas와 같은지
kubectl scale statefulset스케일 업·다운(역순 삭제)kubectl scale statefulset mysql --replicas=5
kubectl set image statefulset/이미지 롤링 업데이트kubectl set image statefulset/mysql mysql=mysql:8.0.36
kubectl patch statefulsetpartition 카나리 업데이트 설정... -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl get pvc파드별 독립 PVC Bound 확인data-mysql-0 등 ordinal별 PVC
kubectl describe pvcPVC Pending 원인(이벤트) 확인... | grep -A10 Events (StorageClass 없음 등)
kubectl get storageclass사용 가능한 StorageClass 확인비어 있으면 동적 프로비저닝 불가
kubectl run --rm -it -- nslookupHeadless Service DNS 확인nslookup mysql-0.mysql.<ns>.svc.cluster.local
kubectl get endpoints -o yamlnot-Ready 주소 게시 여부 진단publishNotReadyAddresses 필요 여부 확인

관련 모듈로 더 깊이:

다음 모듈 pod-disruption-budget에서는 StatefulSet이나 Deployment의 파드를 유지보수 중에도 안전하게 보호하는 방법을 다룹니다. 노드 드레인 시 최소 가용 파드 수를 보장하는 PDB 설정으로 무중단 운영의 마지막 안전망을 구성합니다.

지식 확인

퀴즈 — 8문제

Q1

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

Q2

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

Q3

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

Q4

로그 수집 DaemonSet을 SSD가 달린 노드(ssd=true 레이블)에만 띄우려면 어떻게 하는가?

Q5

StatefulSet에서 mysql-0이 Pending에 오래 멈춰 있으면 mysql-1, mysql-2는 어떻게 되는가?

Q6

StatefulSet의 각 파드가 재시작·재스케줄돼도 같은 데이터 볼륨에 다시 붙는 이유는?

Q7

[심화] StatefulSet + Headless Service로 클러스터형 앱을 처음 띄울 때, 부팅 중인 파드끼리 DNS로 서로를 찾게 하려면 Headless Service에 무엇이 필요한가?

Q8

[심화] 클러스터형 StatefulSet의 모든 파드가 0/1 not-Ready에 멈췄고 로그에 피어 DNS 조회 실패가 반복된다. PVC는 Bound다. 원인을 확정하려면 무엇을 보나?

0 / 8 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

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

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

이것도 배워보세요