Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Stateful Service 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Stateful Service

Luuuuu 2025. 10. 11. 21:00

 

쿠버네티스는 기본적으로 스테이트리스(Stateless) 애플리케이션을 위해 설계되었습니다. Deployment로 배포하면 Pod가 무작위 이름으로 생성되고, 언제든 교체 가능하며, 공유 스토리지를 사용하죠. 하지만 현실은 어떨까요?

실무에서는 데이터베이스, 메시지 큐, 분산 캐시 등 상태를 유지해야 하는 애플리케이션이 필수적입니다. 이런 애플리케이션들은:

  • 각 인스턴스가 고유한 신원(identity)을 가져야 하고
  • 재시작 후에도 동일한 네트워크 주소를 유지해야 하며
  • 각자의 영구 스토리지를 가져야 하고
  • 시작과 종료 순서가 보장되어야 합니다

바로 이런 요구사항을 해결하기 위해 쿠버네티스는 StatefulSet을 제공합니다.

이 글에서는 StatefulSet을 테스트하면서 마주친 문제들과 해결 방법을 공유하겠습니다.

왜 Deployment로는 부족한가?

먼저 Deployment의 한계를 살펴보겠습니다.

Deployment의 문제점

# 잘못된 예: Deployment로 MongoDB 배포
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongodb
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: mongodb
        image: mongo:6.0
        volumeMounts:
        - name: data
          mountPath: /data/db
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: mongodb-pvc  # 모든 Pod가 동일한 PVC 공유!

문제점:

  1. Pod 이름이 mongodb-7c9f6d8b4-xyz12 같은 무작위 해시값
  2. Pod 재시작 시 이름과 IP가 변경됨
  3. 모든 Pod가 동일한 스토리지를 공유하거나, 각자 설정이 복잡
  4. 시작/종료 순서가 보장되지 않음

이런 문제는 특히 클러스터형 데이터베이스에서 문제로 작용할 수 있습니다. MongoDB Replica Set이나 Kafka 클러스터는 각 노드가 고유한 ID와 안정적인 네트워크 주소를 필요로 하기 때문입니다.

StatefulSet 구현

그럼 실제로 StatefulSet을 구현 및 테스트 해보겠습니다. 로그를 영구 스토리지에 저장하는 random-generator 애플리케이션을 예제로 사용하겠습니다.

1단계: PersistentVolume 생성

먼저 각 Pod가 사용할 스토리지를 준비합니다.

# pvs.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: log-1
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Mi
  storageClassName: hostpath  # Docker Desktop 환경
  hostPath:
    path: /tmp/k8s-patterns/stateful-service/1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: log-2
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Mi
  storageClassName: hostpath
  hostPath:
    path: /tmp/k8s-patterns/stateful-service/2

프로덕션에서는 hostPath 대신 클라우드 프로바이더의 스토리지(EBS, GCE PD 등)를 사용하세요.

2단계: Headless Service 생성

StatefulSet은 반드시 Headless Service가 필요합니다. 이를 통해 각 Pod에 안정적인 DNS 이름을 부여합니다.

# service.yml
apiVersion: v1
kind: Service
metadata:
  name: random-generator
  labels:
    app: random-generator
spec:
  clusterIP: None  # Headless Service의 핵심!
  selector:
    app: random-generator
  ports:
  - name: http
    port: 8080
    targetPort: 8080

이렇게 하면 다음과 같은 DNS 패턴으로 각 Pod에 접근할 수 있습니다:

  • rg-0.random-generator.default.svc.cluster.local
  • rg-1.random-generator.default.svc.cluster.local

3단계: StatefulSet 정의

이제 핵심인 StatefulSet을 만듭니다.

# statefulset.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rg
  labels:
    app: random-generator
spec:
  serviceName: random-generator  # Headless Service와 연결
  replicas: 2
  selector:
    matchLabels:
      app: random-generator
  template:
    metadata:
      labels:
        app: random-generator
    spec:
      # fsGroup 설정으로 볼륨 권한 문제 해결
      securityContext:
        fsGroup: 1000

      # initContainer로 로그 디렉토리 권한 설정
      initContainers:
      - name: fix-permissions
        image: busybox:1.35
        command: ['sh', '-c', 'chmod 777 /opt/logs']
        volumeMounts:
        - name: logs
          mountPath: /opt/logs

      containers:
      - name: random-generator
        image: k8spatterns/random-generator:1.0
        env:
        - name: LOG_FILE
          value: /opt/logs/random.log
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        ports:
        - containerPort: 8080
          name: http
        volumeMounts:
        - name: logs
          mountPath: /opt/logs
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

  # volumeClaimTemplates: 각 Pod마다 독립적인 PVC 자동 생성
  volumeClaimTemplates:
  - metadata:
      name: logs
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: hostpath
      resources:
        requests:
          storage: 10Mi

핵심 포인트

  1. serviceName: Headless Service와 연결
  2. volumeClaimTemplates: 각 Pod마다 자동으로 PVC 생성
    • logs-rg-0, logs-rg-1이 자동 생성됨
  3. initContainer: 볼륨 권한 문제를 사전에 해결
  4. securityContext: fsGroup으로 그룹 권한 설정

배포 및 검증

배포하기

# 1. PersistentVolume 생성
kubectl apply -f pvs.yml

# 2. Headless Service 생성
kubectl apply -f service.yml

# 3. StatefulSet 배포
kubectl apply -f statefulset.yml

# 4. NodePort Service 생성 (외부 접근용)
kubectl apply -f service-nodeport.yml

배포 결과 확인

$ kubectl get pods -l app=random-generator
NAME   READY   STATUS    RESTARTS   AGE
rg-0   1/1     Running   0          2m
rg-1   1/1     Running   0          1m30s

중요: Pod가 순차적으로 생성됩니다. rg-0이 완전히 Ready 상태가 된 후에 rg-1이 시작됩니다.

$ kubectl get pvc -l app=random-generator
NAME        STATUS   VOLUME   CAPACITY
logs-rg-0   Bound    log-1    10Mi
logs-rg-1   Bound    log-2    10Mi

각 Pod마다 독립적인 PVC가 자동으로 생성되었습니다!

$ kubectl get svc
NAME                  TYPE        CLUSTER-IP    PORT(S)
random-generator      ClusterIP   None          8080/TCP
random-generator-np   NodePort    10.96.80.22   8080:30704/TCP

 

마주친 문제들과 해결 방법

문제 1: Permission Denied 에러

증상:

2025-10-11 05:35:23.214 ERROR 1 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]
java.io.FileNotFoundException: /opt/logs/random.log (Permission denied)

Pod가 계속 재시작되고 Readiness Probe가 실패했습니다.

원인:

  • PersistentVolume의 디렉토리가 root 소유로 생성됨
  • 컨테이너 내부 프로세스(uid 1000)가 쓰기 권한이 없음

해결 방법:

  1. securityContext에 fsGroup 추가
  2. spec: securityContext: fsGroup: 1000 # 볼륨을 gid 1000으로 마운트
  3. initContainer로 권한 설정
    이 두 가지를 조합하니 권한 문제가 완전히 해결되었습니다.

 

문제 2: StorageClass 불일치

증상:

Warning  ProvisioningFailed  storageclass.storage.k8s.io "standard" not found

PVC가 Pending 상태에서 멈췄습니다.

원인:

  • GitHub 예제는 Minikube 환경(StorageClass: standard)을 가정
  • Docker Desktop Kubernetes는 hostpath StorageClass 사용

해결 방법:

# 사용 가능한 StorageClass 확인
$ kubectl get storageclass
NAME                 PROVISIONER
hostpath (default)   docker.io/hostpath

모든 YAML 파일에서 storageClassNamehostpath로 변경:

# pvs.yml
spec:
  storageClassName: hostpath  # standard → hostpath

# statefulset.yml
volumeClaimTemplates:
- spec:
    storageClassName: hostpath  # standard → hostpath

 

각 환경의 기본 StorageClass를 확인하고 적절히 사용하세요.

  • Minikube: standard
  • Docker Desktop: hostpath
  • GKE: standard (GCE PD)
  • EKS: gp2 (AWS EBS)
  • AKS: default (Azure Disk)

문제 3: PV Released 상태

증상:

NAME    STATUS     CLAIM
log-1   Released   default/logs-rg-0

PVC를 삭제하고 재생성했더니 PV가 Released 상태로 남아서 새 PVC가 바인딩되지 않았습니다.

원인:

  • PV의 기본 reclaimPolicyRetain
  • 이전 PVC의 데이터가 남아있어서 보안상 자동 재사용 불가

해결 방법:

# Released 상태의 PV 삭제 후 재생성
kubectl delete pv log-1 log-2
kubectl apply -f pvs.yml

또는 PV를 재사용하려면:

# PV의 claimRef 제거
kubectl patch pv log-1 -p '{"spec":{"claimRef": null}}'

기능 검증

이제 StatefulSet의 핵심 기능들이 제대로 작동하는지 검증해보겠습니다.

1. Persistent Identity

$ kubectl get pods -l app=random-generator
NAME   READY   STATUS
rg-0   1/1     Running
rg-1   1/1     Running

- Pod 이름이 순서대로 생성: rg-0, rg-1

# Pod 삭제 후 재생성
$ kubectl delete pod rg-0
pod "rg-0" deleted

$ kubectl get pods -l app=random-generator
NAME   READY   STATUS
rg-0   1/1     Running   # 동일한 이름으로 재생성!
rg-1   1/1     Running

- 동일한 이름으로 재생성 확인

2.Dedicated Storage

$ kubectl get pvc -l app=random-generator
NAME        STATUS   VOLUME   CAPACITY
logs-rg-0   Bound    log-1    10Mi
logs-rg-1   Bound    log-2    10Mi

- 각 Pod마다 독립적인 PVC 자동 생성

3. Data Persistence

Pod 삭제 전 로그 확인:

$ kubectl exec rg-0 -- cat /opt/logs/random.log | head -3
11:42:56.449,f446e257-3e18-473c-96f3-9fd4117b72bf,16459,-107823759
11:43:01.388,f446e257-3e18-473c-96f3-9fd4117b72bf,20083,1643007215
11:43:06.384,f446e257-3e18-473c-96f3-9fd4117b72bf,7875,226405971

Pod 삭제 후 재생성:

$ kubectl delete pod rg-0
$ sleep 30

로그 다시 확인:

$ kubectl exec rg-0 -- cat /opt/logs/random.log | head -5
11:42:56.449,f446e257-3e18-473c-96f3-9fd4117b72bf,16459,-107823759  # 기존 데이터!
11:43:01.388,f446e257-3e18-473c-96f3-9fd4117b72bf,20083,1643007215  # 기존 데이터!
11:43:06.384,f446e257-3e18-473c-96f3-9fd4117b72bf,7875,226405971   # 기존 데이터!
11:43:11.385,f446e257-3e18-473c-96f3-9fd4117b72bf,9500,887873635   # 새 데이터
11:43:16.388,f446e257-3e18-473c-96f3-9fd4117b72bf,16750,626974804  # 새 데이터

- Pod 재생성 후에도 기존 로그 데이터가 완벽하게 유지됨!

각 Pod의 로그를 비교해보면:

# rg-0의 UUID
$ kubectl exec rg-0 -- cat /opt/logs/random.log | head -1
11:42:56.449,f446e257-3e18-473c-96f3-9fd4117b72bf,16459,-107823759

# rg-1의 UUID (다름!)
$ kubectl exec rg-1 -- cat /opt/logs/random.log | head -1
11:40:25.462,42f57721-2912-4177-9180-f06b8428aac0,17125,2021406239

- 각 Pod가 독립적인 데이터를 가지고 있음을 확인

4. Stable Network

# Headless Service 확인
$ kubectl get svc random-generator
NAME               TYPE        CLUSTER-IP   PORT(S)
random-generator   ClusterIP   None         8080/TCP

DNS 레코드 확인:

$ kubectl run -it --rm debug --image=busybox --restart=Never -- \
  nslookup rg-0.random-generator.default.svc.cluster.local

Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      rg-0.random-generator.default.svc.cluster.local
Address 1: 10.1.0.28 rg-0.random-generator.default.svc.cluster.local

- 각 Pod에 안정적인 DNS 이름 할당

5. Ordinality(순서 보장)

StatefulSet 배포 과정을 관찰하면:

$ kubectl get pods -l app=random-generator -w
NAME   READY   STATUS
rg-0   0/1     ContainerCreating
rg-0   0/1     Running
rg-0   1/1     Running          # rg-0이 Ready된 후
rg-1   0/1     Pending          # rg-1 생성 시작
rg-1   0/1     ContainerCreating
rg-1   1/1     Running

- 순차적 시작 확인 (rg-0 완료 후 rg-1 시작)

StatefulSet vs Deployment 비교

특징 StatefulSet Deployment
Pod 이름 순서 보장 (rg-0, rg-1) 무작위 해시 (app-xyz)
네트워크 ID 안정적 DNS 이름 불안정
스토리지 각 Pod별 독립 PVC 공유 또는 수동 설정
시작/종료 순서 순차적 병렬
사용 사례 DB, 메시지 큐, 캐시 웹 서버, API 서버
복잡도 높음 낮음

Operator 패턴으로의 진화

StatefulSet만으로는 복잡한 애플리케이션 운영이 어려울 수 있습니다.

이 경우 Operator 패턴을 고려해야 합니다.

주요 Operator들

애플리케이션 Operator 추가 기능
PostgreSQL CloudNativePG 자동 백업, 복제 관리
MongoDB MongoDB Community Operator Replica Set 자동 구성
Elasticsearch ECK 클러스터 자동 확장
Kafka Strimzi 토픽 관리, 모니터링
Redis Redis Enterprise Operator 샤딩, 페일오버

 

Operator는 StatefulSet 위에 다음을 추가로 제공합니다:

  • 자동 백업 및 복원
  • 클러스터 자동 구성
  • 롤링 업그레이드 자동화
  • 장애 자동 복구
  • 스케일링 자동화

참고 자료