소개
Deployment 에서는 복제본을 서로 대체 가능한 대상으로 취급합니다. 하지만 상태 저장 애플리케이션은 안정적인 Pod 이름, 예측 가능한 시작 순서, Pod 가 교체되어도 유지되는 스토리지가 필요한 경우가 많습니다. StatefulSet 은 각 Pod 에 고유한 식별자를 부여하고, Pod 별 스토리지 요청을 함께 관리합니다.
이 실습에서는 동적 스토리지를 확인하고, 복제본 2 개로 구성된 StatefulSet 을 배포한 다음 데이터를 기록합니다. 이후 Pod 를 교체하여 식별자와 데이터가 그대로 유지되는지 검증합니다.
다음 세 가지 식별자를 서로 구분하여 살펴보세요. StatefulSet 은 원하는 복제본 수를 관리하고, 각 Pod 에는 -0과 같은 안정적인 순번이 부여되며, 각 순번에는 고유한 PersistentVolumeClaim 이 연결됩니다. Pod 가 교체되더라도 PVC 와 저장된 데이터는 유지됩니다.
동적 스토리지 확인
이 단계에서는 스토리지를 요청하기 전에 클러스터의 StorageClass 를 확인합니다.
PersistentVolumeClaim 은 워크로드에 필요한 스토리지를 정의하고, StorageClass 는 클러스터가 해당 스토리지를 프로비저닝하는 방법을 정의합니다. 먼저 사용 가능한 클래스를 확인하면 바인딩될 수 없는 PVC 를 작성하는 실수를 예방할 수 있습니다.
cd /home/labex/project/stateful-storage
kubectl get storageclasses
kubectl get storageclass -o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'
PVC 에서 storageClassName을 생략하면 기본 StorageClass 가 동적으로 PersistentVolume 을 생성합니다. 따라서 학습용 매니페스트에서 노드별 hostPath 설정을 직접 지정할 필요가 없습니다.
일반 출력에서는 (default)가 표시되는 항목을, 사용자 지정 DEFAULT 열에서는 true가 표시되는 항목을 찾으세요. 프로비저너 이름은 이 환경의 구현 세부 사항이므로, 기본 클래스가 존재한다는 사실이 중요합니다.
이 환경에서는 다음과 같은 결과가 출력됩니다.
NAME PROVISIONER ...
standard (default) k8s.io/minikube-hostpath ...
NAME PROVISIONER DEFAULT
standard k8s.io/minikube-hostpath true
따라서 standard가 기본 클래스입니다. StatefulSet 에서 storageClassName을 지정하지 않으므로 Kubernetes 가 이 클래스를 자동으로 선택합니다.
StatefulSet 및 헤드리스 Service 생성
이 단계에서는 안정적인 DNS 를 제공하는 헤드리스 Service 와 복제본마다 하나의 PVC 를 생성하는 StatefulSet 을 만듭니다.
이 매니페스트에는 서로 연관된 여러 필드가 포함되어 있습니다. 적용하기 전에 각 필드의 역할을 확인하세요.
clusterIP: None은 Service 를 헤드리스로 설정하여 DNS 가 개별 Pod 를 나타낼 수 있도록 합니다.serviceName: stateful-web은 StatefulSet 을 해당 Service 에 연결합니다.volumeClaimTemplates는 템플릿 이름과 Pod 순번을 조합하여 복제본마다 하나의 PVC 를 생성합니다.volumeMounts는 각 Pod 의 전용 PVC 를 NGINX 의 문서 디렉터리에 마운트합니다.
레이블은 첫 번째 과정에서 익힌 것처럼 Service 가 대상 Pod 를 선택하는 데 계속 사용됩니다.
cd /home/labex/project/stateful-storage
cat <<'EOF' > stateful-web.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-stateful}
---
apiVersion: v1
kind: Service
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
clusterIP: None
selector: {app: stateful-web}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
serviceName: stateful-web
replicas: 2
selector: {matchLabels: {app: stateful-web}}
template:
metadata: {labels: {app: stateful-web}}
spec:
containers:
- name: web
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports: [{name: http, containerPort: 80}]
volumeMounts: [{name: web-data, mountPath: /usr/share/nginx/html}]
volumeClaimTemplates:
- metadata: {name: web-data}
spec:
accessModes: [ReadWriteOnce]
resources: {requests: {storage: 100Mi}}
EOF
매니페스트를 적용하고 순서에 따른 롤아웃이 완료될 때까지 기다립니다. StatefulSet 은 일반적으로 순번 0인 Pod 를 먼저 생성한 뒤 순번 1인 Pod 를 생성하므로, 두 번째 Pod 가 조금 늦게 나타날 수 있습니다.
kubectl apply -f stateful-web.yaml
kubectl rollout status statefulset/stateful-web -n practice-stateful --timeout=90s
kubectl get pods,pvc -n practice-stateful
Pod 이름에는 안정적인 순번이 사용됩니다. 예를 들어 stateful-web-0, stateful-web-1과 같은 이름이 지정됩니다. 각 PVC 이름에는 템플릿 이름, Pod 이름, 순번이 포함됩니다.
web-data-stateful-web-0과 web-data-stateful-web-1이라는 PVC 가 생성되고 두 항목 모두 Bound 상태가 되어야 합니다. Bound는 각 요청을 충족하는 스토리지를 Kubernetes 가 찾았거나 동적으로 프로비저닝했다는 뜻입니다.
롤아웃 메시지에서 순서에 따른 동작을 확인할 수 있으며, 이어서 두 PVC 가 표시됩니다.
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
pod/stateful-web-0 1/1 Running ...
pod/stateful-web-1 1/1 Running ...
persistentvolumeclaim/web-data-stateful-web-0 Bound ... 100Mi RWO standard
persistentvolumeclaim/web-data-stateful-web-1 Bound ... 100Mi RWO standard
생성되는 볼륨 ID 는 실행할 때마다 달라집니다. 안정적인 PVC 이름, Bound 상태, 요청된 용량, 스토리지 클래스에 주목하세요.
Pod 별 영구 데이터 기록
이 단계에서는 각 Pod 에 연결된 PVC 에 서로 다른 내용을 기록합니다.
출력 리디렉션 (>) 이 컨테이너 내부에서 실행되어야 하므로 kubectl exec ... -- sh -c를 사용합니다. sh -c가 없으면 로컬 셸이 Pod 내부가 아닌 VM 에서 출력을 리디렉션하려고 합니다.
cd /home/labex/project/stateful-storage
kubectl exec stateful-web-0 -n practice-stateful -- sh -c 'echo persistent-zero > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-1 -n practice-stateful -- sh -c 'echo persistent-one > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl exec stateful-web-1 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pods -n practice-stateful -o custom-columns='POD:.metadata.name,HOSTNAME:.spec.hostname,CLAIM:.spec.volumes[0].persistentVolumeClaim.claimName'
두 Pod 는 같은 이미지를 실행하지만 서로 다른 PVC 를 연결하므로 데이터는 서로 공유되지 않습니다.
두 cat 명령은 서로 다른 문자열을 출력해야 합니다. 이어서 사용자 지정 표를 통해 각 안정적인 Pod 이름이 어느 PVC 에 연결되어 있는지 확인할 수 있습니다.
persistent-zero
persistent-one
POD HOSTNAME CLAIM
stateful-web-0 stateful-web-0 web-data-stateful-web-0
stateful-web-1 stateful-web-1 web-data-stateful-web-1
세 열 모두에 순번이 표시됩니다. 반복해서 나타나는 0 또는 1이 바로 Pod 와 해당 Pod 전용 스토리지를 연결하는 식별자입니다.
Pod 교체 후 데이터 복구
이 단계에서는 순번 0 인 Pod 를 삭제하고 StatefulSet 이 동일한 식별자로 다시 생성하도록 합니다.
관리 대상 Pod 를 삭제하는 것은 원하는 복제본 수를 줄이는 것과 다릅니다. StatefulSet 은 여전히 Pod 2 개를 요구하므로 stateful-web-0이라는 이름의 대체 Pod 를 생성하고 해당 순번에 예약된 PVC 를 다시 연결합니다.
cd /home/labex/project/stateful-storage
kubectl delete pod stateful-web-0 -n practice-stateful --wait=true
kubectl wait --for=condition=Ready pod/stateful-web-0 -n practice-stateful --timeout=90s
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pod stateful-web-0 -n practice-stateful -o jsonpath='pod={.metadata.name} claim={.spec.volumes[0].persistentVolumeClaim.claimName}{"\n"}'
Pod 객체 자체는 새로 생성되지만 이름과 PVC 연결 관계는 유지됩니다. 복구된 파일은 데이터가 삭제된 컨테이너 파일 시스템이 아니라 PersistentVolume 에 저장되어 있었다는 사실을 증명합니다.
pod "stateful-web-0" deleted from practice-stateful namespace
pod/stateful-web-0 condition met
persistent-zero
pod=stateful-web-0 claim=web-data-stateful-web-0
핵심은 persistent-zero라는 출력입니다. 이 값은 Pod 를 삭제하기 전에 기록되었으며, 교체된 Pod 에서 다시 읽어 들인 것입니다.
마운트된 디렉터리 외부에 파일을 기록했다면 기존 컨테이너 파일 시스템과 함께 사라졌을 것입니다. 데이터의 지속성은 StatefulSet 이름만으로 결정되는 것이 아니라, 파일이 기록된 마운트 경로에 달려 있습니다.
요약
기본 StorageClass, 동적 PVC 프로비저닝, 헤드리스 Service, StatefulSet 의 안정적인 순번, Pod 별 스토리지를 하나의 흐름으로 연결해 보았습니다. Pod 를 교체한 뒤에도 식별자와 데이터가 모두 유지되는 것을 확인했습니다.


