StatefulSet이란? Deployment와의 핵심 차이
StatefulSet은 Kubernetes에서 상태가 있는(Stateful) 워크로드를 관리하는 컨트롤러다. Deployment가 모든 Pod를 동일하게 취급하는 반면, StatefulSet은 각 Pod에 고유한 이름, 고정 네트워크 ID, 전용 스토리지를 부여한다. 데이터베이스, 메시지 큐, 분산 캐시처럼 각 인스턴스가 자신만의 데이터와 정체성을 유지해야 하는 워크로드에 필수적이다.
이 글에서는 StatefulSet의 동작 원리, Headless Service, PersistentVolumeClaim 템플릿, 순서 보장, 업데이트 전략, 그리고 실무 운영 패턴까지 깊이 있게 다룬다.
StatefulSet의 3가지 보장
StatefulSet이 Deployment와 구별되는 핵심 보장은 다음 세 가지다:
| 보장 | Deployment | StatefulSet |
|---|---|---|
| Pod 이름 | 랜덤 해시 (app-7b9f4d) | 순서 인덱스 (app-0, app-1, app-2) |
| 네트워크 ID | 매번 변경 | 고정 DNS (app-0.svc.cluster.local) |
| 스토리지 | 공유 또는 임시 | Pod별 전용 PVC (재스케줄링 시에도 유지) |
| 생성/삭제 순서 | 병렬 | 순차 (0→1→2 생성, 2→1→0 삭제) |
기본 StatefulSet 매니페스트
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: database
spec:
serviceName: postgres-headless # Headless Service 이름 (필수)
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: tcp-postgres
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 30
periodSeconds: 10
volumeClaimTemplates: # Pod별 자동 PVC 생성
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
volumeClaimTemplates가 핵심이다. StatefulSet은 각 Pod마다 별도의 PVC를 자동 생성한다: data-postgres-0, data-postgres-1, data-postgres-2. Pod가 삭제되고 재생성되어도 동일한 PVC에 다시 바인딩되므로 데이터가 보존된다.
Headless Service: 고정 DNS 엔드포인트
StatefulSet은 반드시 Headless Service(clusterIP: None)와 함께 사용한다. 일반 Service가 로드밸런싱된 단일 IP를 제공하는 반면, Headless Service는 각 Pod의 개별 DNS 레코드를 생성한다.
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: database
spec:
clusterIP: None # Headless!
selector:
app: postgres
ports:
- port: 5432
targetPort: tcp-postgres
name: tcp-postgres
---
# 클라이언트용 일반 Service (읽기 로드밸런싱)
apiVersion: v1
kind: Service
metadata:
name: postgres-read
namespace: database
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: tcp-postgres
생성되는 DNS 레코드:
# 개별 Pod DNS (Headless Service 경유)
postgres-0.postgres-headless.database.svc.cluster.local
postgres-1.postgres-headless.database.svc.cluster.local
postgres-2.postgres-headless.database.svc.cluster.local
# Primary 연결 (특정 Pod 지정)
jdbc:postgresql://postgres-0.postgres-headless.database.svc.cluster.local:5432/mydb
# 읽기 로드밸런싱 (일반 Service 경유)
jdbc:postgresql://postgres-read.database.svc.cluster.local:5432/mydb
이 패턴이 중요한 이유: DB 복제(Replication)에서 Primary는 postgres-0에 고정하고, 나머지를 Replica로 운영할 수 있다. 애플리케이션은 쓰기는 postgres-0으로, 읽기는 postgres-read Service로 분산한다.
Pod 관리 정책: OrderedReady vs Parallel
기본값인 OrderedReady는 Pod를 순서대로 생성하고 역순으로 삭제한다. 하지만 모든 워크로드에 순서 보장이 필요한 것은 아니다.
spec:
podManagementPolicy: Parallel # 모든 Pod 동시 생성/삭제
# podManagementPolicy: OrderedReady # 기본값: 순차 생성
| 정책 | 동작 | 적합 사례 |
|---|---|---|
OrderedReady |
0→1→2 순차, 이전 Pod Ready 후 다음 생성 | DB Primary/Replica, ZooKeeper |
Parallel |
모든 Pod 동시 생성/삭제 | Elasticsearch, Cassandra (피어 대등) |
업데이트 전략: RollingUpdate와 Partition
StatefulSet의 롤링 업데이트는 역순(높은 인덱스→낮은 인덱스)으로 진행된다. partition 필드를 활용하면 카나리 배포를 구현할 수 있다.
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 인덱스 >= 2인 Pod만 업데이트
maxUnavailable: 1 # K8s 1.24+: 동시 업데이트 Pod 수
Partition 기반 카나리 배포 워크플로우:
# 1단계: partition=2 → postgres-2만 새 버전으로 업데이트
kubectl patch statefulset postgres -n database
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image statefulset/postgres postgres=postgres:17-alpine -n database
# 2단계: postgres-2 정상 확인 후 partition=1
kubectl patch statefulset postgres -n database
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}'
# 3단계: 전체 롤아웃 (partition=0)
kubectl patch statefulset postgres -n database
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
# 롤백: 이미지만 되돌리면 partition 이상 Pod가 롤백됨
kubectl set image statefulset/postgres postgres=postgres:16-alpine -n database
스케일링: PVC 생명주기 주의사항
StatefulSet을 스케일다운하면 Pod는 삭제되지만 PVC는 자동 삭제되지 않는다. 이는 데이터 보호를 위한 의도적 설계다.
# 3 → 2로 스케일다운
kubectl scale statefulset postgres --replicas=2 -n database
# postgres-2 Pod 삭제됨, BUT data-postgres-2 PVC는 남아 있음
# 다시 3으로 스케일업하면 기존 PVC에 재바인딩
kubectl scale statefulset postgres --replicas=3 -n database
# postgres-2가 data-postgres-2 PVC를 다시 사용
# PVC 수동 정리 (데이터 삭제 확인 후!)
kubectl delete pvc data-postgres-2 -n database
# K8s 1.27+: PVC 자동 삭제 정책
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete # StatefulSet 삭제 시 PVC도 삭제
whenScaled: Retain # 스케일다운 시 PVC 유지 (기본값)
Init Container로 클러스터 부트스트랩
DB 클러스터 구성 시 Pod 인덱스에 따라 역할을 분기하는 것이 일반적이다. Init Container에서 hostname 명령으로 인덱스를 추출한다.
spec:
template:
spec:
initContainers:
- name: init-config
image: busybox:1.36
command:
- sh
- -c
- |
# Pod 인덱스 추출: postgres-0 → 0
ORDINAL=$(hostname | grep -o '[0-9]*$')
if [ "$ORDINAL" -eq 0 ]; then
echo "primary" > /config/role
cat /etc/postgres/primary.conf > /config/postgresql.conf
else
echo "replica" > /config/role
cat /etc/postgres/replica.conf > /config/postgresql.conf
# Primary DNS로 replication 설정
echo "primary_conninfo = 'host=postgres-0.postgres-headless port=5432'"
>> /config/postgresql.conf
fi
echo "server_id = $((ORDINAL + 1))" >> /config/postgresql.conf
volumeMounts:
- name: config
mountPath: /config
- name: config-templates
mountPath: /etc/postgres
containers:
- name: postgres
volumeMounts:
- name: config
mountPath: /etc/postgresql/conf.d
실전 예시: Redis Cluster StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-cluster-headless
replicas: 6 # 3 master + 3 replica
podManagementPolicy: Parallel
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command:
- redis-server
- /etc/redis/redis.conf
- --cluster-enabled
- "yes"
- --cluster-config-file
- /data/nodes.conf
- --cluster-node-timeout
- "5000"
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/redis
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
readinessProbe:
tcpSocket:
port: client
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
---
# 클러스터 초기화 Job
apiVersion: batch/v1
kind: Job
metadata:
name: redis-cluster-init
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: init
image: redis:7-alpine
command:
- sh
- -c
- |
sleep 30 # 모든 Pod Ready 대기
HOSTS=""
for i in $(seq 0 5); do
HOSTS="$HOSTS redis-cluster-$i.redis-cluster-headless:6379"
done
redis-cli --cluster create $HOSTS
--cluster-replicas 1 --cluster-yes
PodDisruptionBudget: 안전한 노드 유지보수
StatefulSet 워크로드는 데이터 정합성이 중요하므로, 노드 드레인 시 동시에 너무 많은 Pod가 종료되지 않도록 PDB를 반드시 설정한다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: postgres-pdb
namespace: database
spec:
minAvailable: 2 # 최소 2개 Pod 유지
# 또는: maxUnavailable: 1 # 최대 1개만 동시 종료
selector:
matchLabels:
app: postgres
운영 체크리스트
- StorageClass 확인:
volumeBindingMode: WaitForFirstConsumer로 설정해야 Pod가 스케줄된 노드의 Zone에서 PV가 프로비저닝된다. 그렇지 않으면 다른 Zone의 PV에 바인딩되어 Pod가 스케줄링 실패한다 - Topology 분산: Pod Topology Spread Constraints로 Pod를 여러 Zone/노드에 분산 배치한다
- 백업: PVC 스냅샷(VolumeSnapshot)을 주기적으로 생성한다. StatefulSet 삭제가 PVC를 자동 삭제하지 않더라도, 실수 대비는 필수다
- Graceful Shutdown:
terminationGracePeriodSeconds를 충분히 설정하여 DB가 안전하게 종료할 시간을 확보한다 - 리소스 설정: Resource requests/limits를 정확히 설정하여 QoS를 Guaranteed로 유지한다. DB Pod가 Burstable이면 OOMKill 위험이 있다
- PVC 용량 확장:
allowVolumeExpansion: true인 StorageClass를 사용하면 PVC 용량을 온라인으로 확장할 수 있다
StatefulSet은 Kubernetes에서 상태를 관리하는 핵심 컨트롤러다. Headless Service로 고정 DNS를 확보하고, volumeClaimTemplates로 Pod별 전용 스토리지를 보장하며, Partition 기반 카나리 배포로 안전하게 업데이트할 수 있다. 단, 스토리지 생명주기와 순서 보장 특성을 정확히 이해하지 않으면 데이터 유실이나 스케줄링 실패로 이어질 수 있으므로, 운영 전 충분한 테스트가 필수다.