Kubernetes에서의 StatefulSets 및 지속 가능한 저장소

순차적 확장 및 지속 가능한 데이터와 함께 상태 있는 앱 배포

Kubernetes StatefulSets은 안정적인 정체성, 지속 가능한 저장소, 순서 있는 배포 패턴이 필요한 상태가 있는 애플리케이션을 관리하는 데 이상적인 솔루션입니다. 데이터베이스, 분산 시스템, 캐싱 레이어와 같은 필수적인 작업에 사용됩니다.

Kubernetes에 처음 접하거나 클러스터를 설정 중이라면, 개발 환경에서는 k3s나 MicroK8s와 같은 Kubernetes 배포판을 고려하거나, 프로덕션 등급의 클러스터를 설치하려면 Kubespray로 Kubernetes 설치을 참고하세요.

카페에서 발표 이 아름다운 이미지는 AI 모델 Flux 1 dev에 의해 생성되었습니다.

StatefulSets란 무엇인가요?

StatefulSets는 상태가 있는 애플리케이션을 관리하기 위해 설계된 Kubernetes 워크로드 API 객체입니다. 모든 Pod를 동등하게 처리하는 Deployments와 달리, StatefulSets는 각 Pod에 고유한 정체성을 유지하며 순서와 고유성에 대한 보장이 있습니다.

주요 기능:

  • 안정적인 네트워크 식별자: 각 Pod은 재시작을 거치더라도 예측 가능한 호스트 이름을 유지합니다.
  • 지속 가능한 저장소: Pod이 재스케줄링 되더라도 따라가는 PersistentVolumeClaims가 있습니다.
  • 순서 있는 배포: Pod은 순차적으로 생성되며 (0, 1, 2…) 역순으로 종료됩니다.
  • 순서 있는 업데이트: 롤링 업데이트는 순서대로 진행되어 애플리케이션의 안정성을 보장합니다.

PostgreSQL, MySQL, MongoDB, Cassandra, Elasticsearch, Kafka, ZooKeeper, Redis, etcd와 같은 애플리케이션에서 Pod의 정체성과 데이터 지속성이 중요한 경우 StatefulSets는 필수적입니다.

Kubernetes에서의 지속 가능한 저장소 이해

Kubernetes는 Pod의 생명주기에서 저장소 관리를 분리하는 복잡한 저장소 추상화 계층을 제공합니다:

저장소 구성 요소

PersistentVolume (PV): 관리자에 의해 클러스터 내에서 프로비저닝되거나 StorageClass를 통해 동적으로 생성된 저장소의 일부입니다. PV는 Pod과 독립적으로 존재합니다.

PersistentVolumeClaim (PVC): Pod에 의해 요청된 저장소입니다. PVC는 크기, 접근 모드, 저장소 클래스와 같은 요구사항에 맞는 사용 가능한 PV에 바인딩됩니다.

StorageClass: 다양한 프로비저너(AWS EBS, GCE PD, Azure Disk, NFS 등)와 복제, 성능 계층, 백업 정책과 같은 매개변수를 정의하는 저장소의 “종류"를 정의합니다.

접근 모드

  • ReadWriteOnce (RWO): 단일 노드에서 읽기/쓰기로 마운트됩니다.
  • ReadOnlyMany (ROX): 여러 노드에서 읽기 전용으로 마운트됩니다.
  • ReadWriteMany (RWX): 여러 노드에서 읽기/쓰기로 마운트됩니다 (특수한 저장소 백엔드가 필요합니다).

StatefulSet 저장소 아키텍처

StatefulSets는 volumeClaimTemplates를 사용하여 각 Pod 복제본에 대해 자동으로 PersistentVolumeClaims를 생성합니다. 이는 Deployments와 근본적으로 다릅니다:

volumeClaimTemplates 작동 방식

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 10Gi

이 StatefulSet을 생성할 때:

  1. Kubernetes는 Pod mysql-0과 PVC data-mysql-0을 생성합니다.
  2. 그 다음 Pod mysql-1과 PVC data-mysql-1을 생성합니다.
  3. 마지막으로 Pod mysql-2와 PVC data-mysql-2를 생성합니다.

각 Pod은 자체적인 10GB 지속 가능한 볼륨을 받습니다. mysql-1이 삭제되거나 재스케줄링 되면 Kubernetes는 동일한 data-mysql-1 PVC를 다시 연결하여 모든 데이터를 보존합니다.

StatefulSets를 위한 헤드리스 서비스 생성

StatefulSets는 안정적인 네트워크 정체성을 제공하기 위해 헤드리스 서비스가 필요합니다:

apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  clusterIP: None  # 이는 헤드리스 서비스를 만듭니다.
  selector:
    app: mysql
  ports:
  - port: 3306
    name: mysql

이것은 각 Pod에 대한 DNS 엔트리를 생성합니다:

  • mysql-0.mysql-service.default.svc.cluster.local
  • mysql-1.mysql-service.default.svc.cluster.local
  • mysql-2.mysql-service.default.svc.cluster.local

애플리케이션은 이러한 안정적인 DNS 이름을 사용하여 특정 Pod 인스턴스에 직접 연결할 수 있습니다.

StatefulSet 배포 패턴

복잡한 Kubernetes 배포를 관리하는 팀에게 Helm 차트는 템플릿, 버전 관리 및 종속 관리를 통해 StatefulSets를 패키징하고 배포하는 강력한 방법을 제공합니다. Helm은 다양한 환경에서 StatefulSet 구성 관리를 간소화합니다.

순서 있는 확장

3개에서 5개 복제본으로 확장할 때:

kubectl scale statefulset mysql --replicas=5

Kubernetes는 순서대로 Pod을 생성합니다: mysql-3 → Ready 상태가 될 때까지 대기 → mysql-4

5개에서 3개로 축소할 때:

kubectl scale statefulset mysql --replicas=3

Kubernetes는 역순으로 종료합니다: mysql-4 → 종료가 완료될 때까지 대기 → mysql-3

롤링 업데이트

StatefulSets는 두 가지 업데이트 전략을 지원합니다:

OnDelete: 수동 업데이트 - Pod이 삭제될 때만 업데이트됩니다. RollingUpdate: 역순 정렬에 따라 자동으로 순차적으로 업데이트됩니다.

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2  # 2 이상의 ordinal을 가진 Pod만 업데이트

partition 매개변수는 캐니리 배포를 가능하게 합니다 - 먼저 고번호 Pod을 업데이트하고 모든 복제본으로 확장하기 전에 테스트할 수 있습니다.

저장소 최고 실천 방법

동적 프로비저닝

항상 StorageClasses를 사용하여 동적 볼륨 프로비저닝을 수행하세요:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
allowVolumeExpansion: true
reclaimPolicy: Retain

allowVolumeExpansion: PVC를 재생성하지 않고 크기를 조정할 수 있도록 합니다. reclaimPolicy: Retain은 PVC 삭제 후에도 PV 데이터를 보존하고, Delete은 자동으로 삭제합니다.

PVC 보존 정책

Kubernetes 1.23+는 persistentVolumeClaimRetentionPolicy를 지원합니다:

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain    # StatefulSet이 삭제될 때 PVC를 보존합니다.
    whenScaled: Delete     # 축소될 때 PVC를 삭제합니다.

옵션:

  • Retain: PVC를 보존합니다 (기본값, 가장 안전한 방법)
  • Delete: PVC를 자동으로 삭제합니다 (개발 환경에 유용)

백업 전략

볼륨 스냅샷: VolumeSnapshot 리소스를 사용하여 특정 시간의 백업을 생성합니다.

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snapshot-20251113
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: data-mysql-0

애플리케이션 수준 백업: mysqldump, pg_dump, Velero와 같은 도구를 사용하여 데이터베이스 특정 백업을 수행합니다.

분산 복제: MySQL 복제, PostgreSQL 스트리밍 복제와 같은 애플리케이션 수준 복제를 첫 번째 방어선으로 구성합니다.

실제 사용 사례

데이터베이스 클러스터 (PostgreSQL)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        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
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "standard"
      resources:
        requests:
          storage: 20Gi

분산 캐시 (Redis)

Redis 클러스터를 위해 StatefulSet과 주의 깊은 설정이 필요합니다:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-service
  replicas: 6
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        command:
        - redis-server
        - "--appendonly"
        - "yes"
        - "--appendfsync"
        - "everysec"
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi

메시지 큐 (Kafka)

Kafka는 로그에 대한 지속 가능한 저장소와 브로커 조정을 위한 안정적인 네트워크 정체성이 필요합니다:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
spec:
  serviceName: kafka-service
  replicas: 3
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
      - name: kafka
        image: confluentinc/cp-kafka:7.5.0
        ports:
        - containerPort: 9092
          name: kafka
        env:
        - name: KAFKA_BROKER_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: data
          mountPath: /var/lib/kafka/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 100Gi

모니터링 및 문제 해결

이 섹션에서 사용된 Kubernetes 명령어에 대한 종합적인 참조는 Kubernetes 체크리스트를 참조하세요.

StatefulSet 상태 확인

# StatefulSet 세부 정보 보기
kubectl get statefulset mysql
kubectl describe statefulset mysql

# Pod 생성 순서 및 상태 확인
kubectl get pods -l app=mysql -w

# PVC 상태 보기
kubectl get pvc
kubectl describe pvc data-mysql-0

일반적인 문제

Pod이 대기 상태: PVC 상태와 저장소 가용성을 확인하세요

kubectl describe pod mysql-0
kubectl get events --sort-by='.lastTimestamp'

저장소가 가득 차 있음: StorageClass가 허용하는 경우 PVC를 확장하세요

kubectl patch pvc data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

Pod이 종료되지 않음: 애플리케이션 수준 잠금이나 볼륨 언마운트 문제를 확인하세요

kubectl delete pod mysql-0 --grace-period=0 --force

모니터링할 메트릭

  • 저장소 사용량: PVC 용량과 사용률 퍼센트를 모니터링하세요
  • I/O 성능: IOPS, 전송량, 지연 시간을 추적하세요
  • Pod 재시작: 빈번한 재시작은 저장소 문제를 나타낼 수 있습니다
  • PVC 바인딩 시간: 느린 바인딩은 프로비저닝 문제를 나타냅니다

이전 전략

StatefulSets로 마이그레이션할 때 Kubernetes 클러스터가 올바르게 구성되어 있는지 확인하세요. 홈랩이나 작은 클러스터 설정의 경우, 우리의 Kubernetes 배포판의 종합 비교를 참조하여 작업 부하 요구사항에 적합한 플랫폼을 선택하세요.

Deployment에서 StatefulSet으로

  1. volumeClaimTemplates와 함께 StatefulSet 생성
  2. Deployment를 부드럽게 축소
  3. 백업에서 데이터를 StatefulSet Pod으로 복원
  4. DNS/Service 참조 업데이트
  5. 기존 Deployment 및 PVC 삭제

마이그레이션 전 백업

# 기존 PVC 스냅샷
kubectl get pvc -o yaml > pvc-backup.yaml

# 볼륨 스냅샷 생성
kubectl apply -f volume-snapshot.yaml

보안 고려사항

저장소 암호화

StorageClass 매개변수를 사용하여 저장소 암호화를 활성화하세요:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: encrypted-storage
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  encrypted: "true"
  kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/abcd-1234

접근 제어

RBAC를 사용하여 StatefulSets 및 PVC를 생성/수정할 수 있는 사람을 제한하세요:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: statefulset-manager
rules:
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get", "list", "create", "delete"]

네트워크 정책

Pod 간 통신을 제한하세요:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-network-policy
spec:
  podSelector:
    matchLabels:
      app: mysql
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 3306

성능 최적화

저장소 성능 계층

작업 부하에 따라 적절한 StorageClasses를 선택하세요:

  • 높은 IOPS: 무거운 무작위 읽기/쓰기 작업 (gp3, io2)
  • 높은 전송량: 로그 집계, 분석 (st1, sc1)
  • 균형: 일반적인 목적 애플리케이션 (gp3)

Pod 분산

가용성 존에 걸쳐 StatefulSet Pod을 분산시키기 위해 Pod 반대 친화성을 사용하세요:

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
            topologyKey: topology.kubernetes.io/zone

리소스 요청 및 제한

일관된 성능을 위해 적절한 리소스를 설정하세요:

resources:
  requests:
    cpu: "2"
    memory: "4Gi"
    ephemeral-storage: "10Gi"
  limits:
    cpu: "4"
    memory: "8Gi"
    ephemeral-storage: "20Gi"

고급 패턴

초기 컨테이너를 사용한 상태가 있는 애플리케이션

데이터베이스 초기화를 위해 초기 컨테이너를 사용하세요:

spec:
  template:
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:8.0
        command:
        - bash
        - "-c"
        - |
          if [[ ! -d /var/lib/mysql/mysql ]]; then
            mysqld --initialize-insecure --datadir=/var/lib/mysql
          fi          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql

사이드카를 위한 다중 컨테이너 Pod

백업 사이드카 또는 모니터링 에이전트를 추가하세요:

spec:
  template:
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        # ... mysql 설정 ...
      - name: backup-sidecar
        image: mysql-backup:latest
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          readOnly: true

동적 설정을 위한 ConfigMaps

StatefulSet 정의에서 설정을 분리하세요:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  my.cnf: |
    [mysqld]
    max_connections=200
    innodb_buffer_pool_size=2G    
---
spec:
  template:
    spec:
      containers:
      - name: mysql
        volumeMounts:
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config

유용한 링크