Skip to content

Split View: Kubernetes에서 데이터베이스 운영 완전 가이드: StatefulSet, Operator, 백업/복구 전략

✨ Learn with Quiz
|

Kubernetes에서 데이터베이스 운영 완전 가이드: StatefulSet, Operator, 백업/복구 전략

TL;DR

  • StatefulSet: DB처럼 상태가 있는 애플리케이션을 위한 K8s 워크로드 (안정적 네트워크 ID + 영구 스토리지)
  • DB Operator: CloudNativePG(PostgreSQL), Percona Operator(MySQL/MongoDB)로 운영 자동화
  • 스토리지: PV/PVC/StorageClass로 영구 데이터 관리, CSI 드라이버 활용
  • 고가용성: Primary-Replica 구성, 자동 Failover, PodDisruptionBudget
  • 백업/복구: pgBackRest, Velero, PITR(Point-in-Time Recovery) 전략
  • 모니터링: PMM, pg_exporter + Prometheus + Grafana 대시보드

목차

  1. K8s에서 DB를 운영해야 할까?
  2. StatefulSet 심화
  3. 스토리지 전략
  4. Headless Service와 DNS
  5. Database Operator
  6. 고가용성 (HA)
  7. 백업과 복구
  8. 모니터링
  9. 성능 튜닝
  10. 보안
  11. VM에서 K8s로 마이그레이션
  12. 프로덕션 체크리스트
  13. 실전 퀴즈
  14. 참고 자료

1. K8s에서 DB를 운영해야 할까?

1.1 장단점 비교

K8s에서 DB 운영 결정 매트릭스:

운영해야 하는 경우:                    운영하지 말아야 하는 경우:
+ 멀티클라우드/하이브리드 환경          - 관리형 DB 서비스 사용 가능
+ 인프라 일관성이 중요                  - DBA 리소스 부족
+ GitOps/IaC 파이프라인 통합           - 초대형 규모의 단일 DB
+ 개발/테스트 환경 자동화               - 극도로 낮은 레이턴시 요구
+ 비용 최적화가 필수                    - K8s 경험 부족
+ 데이터 주권 규제                      - 단순한 아키텍처로 충분
기준K8s DB 운영관리형 서비스 (RDS/CloudSQL)
초기 설정 복잡도높음낮음
운영 자동화Operator로 가능기본 제공
비용효율적 (리소스 공유)프리미엄 비용
멀티클라우드용이벤더 종속
커스터마이징완전 자유제한적
백업/복구직접 구성 필요기본 제공
확장성수동/반자동자동 확장

1.2 어떤 DB가 K8s에 적합한가?

K8s 친화도 순서:

매우 적합:
  - PostgreSQL (CloudNativePG 생태계 우수)
  - MongoDB (ReplicaSet 구조가 K8s와 자연스러움)
  - Redis (Sentinel/Cluster 모드)

적합:
  - MySQL (Percona/Oracle Operator 사용)
  - Elasticsearch (ECK Operator)
  - Cassandra (K8ssandra Operator)

주의 필요:
  - Oracle DB (라이선스, 복잡성)
  - SQL Server (Windows 컨테이너 제한)
  - 대규모 단일 인스턴스 DB

2. StatefulSet 심화

2.1 StatefulSet vs Deployment

Deployment:                    StatefulSet:
- Pod 이름: random (abc-xyz)   - Pod 이름: 순차적 (db-0, db-1, db-2)
- 병렬 생성/삭제               - 순차적 생성/삭제 (0 -> 1 -> 2)
- 공유 볼륨 또는 없음           - Pod별 전용 PVC (자동 생성)
- 교체 가능한 Pod              - 고유한 ID를 가진 Pod
- Stateless 앱에 적합          - Stateful (DB)에 적합

2.2 StatefulSet YAML 예시

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres-headless  # Headless Service 이름
  replicas: 3
  podManagementPolicy: OrderedReady  # 순차적 생성 (기본값)
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1  # K8s 1.24+
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      terminationGracePeriodSeconds: 120  # DB 종료에 충분한 시간
      securityContext:
        fsGroup: 999       # postgres 그룹
        runAsUser: 999     # postgres 사용자
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
              name: postgresql
          env:
            - name: POSTGRES_DB
              value: myapp
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

2.3 PodManagementPolicy

# OrderedReady (기본값): 순차적 생성/삭제
# Pod 0 Ready -> Pod 1 생성 -> Pod 1 Ready -> Pod 2 생성
podManagementPolicy: OrderedReady

# Parallel: 모든 Pod 동시 생성/삭제
# 초기 부트스트래핑에 유의 (DB는 보통 OrderedReady 사용)
podManagementPolicy: Parallel

2.4 UpdateStrategy

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    # Partition: 이 값 이상의 ordinal Pod만 업데이트
    # 카나리 배포에 활용 (Pod 2만 먼저 업데이트)
    partition: 2

# OnDelete: 수동으로 Pod 삭제 시에만 업데이트
# DB 업그레이드 시 세밀한 제어 가능
updateStrategy:
  type: OnDelete

3. 스토리지 전략

3.1 PV / PVC / StorageClass

# StorageClass 정의 (AWS EBS gp3)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "5000"
  throughput: "250"    # MB/s
  encrypted: "true"
reclaimPolicy: Retain  # DB 데이터는 반드시 Retain!
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true  # 온라인 볼륨 확장 허용
# StorageClass 정의 (GCP PD SSD)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
  replication-type: regional-pd  # 리전 PD (고가용성)
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

3.2 로컬 스토리지 vs 클라우드 볼륨

성능 비교:

Local NVMe SSD:
  - 랜덤 읽기: 500K+ IOPS
  - 레이턴시: 0.1ms 이하
  - 단점: Pod 이동 불가, 노드 장애 시 데이터 손실 위험

Cloud EBS gp3:
  - 기본: 3,000 IOPS / 125 MB/s
  - 최대: 16,000 IOPS / 1,000 MB/s
  - 장점: Pod 이동 가능, 스냅샷 지원

Cloud EBS io2:
  - 최대: 64,000 IOPS
  - 99.999% 내구성
  - 비용이 높지만 미션 크리티컬 DB에 적합

3.3 볼륨 확장

# PVC 크기 확장 (StorageClass에 allowVolumeExpansion: true 필요)
kubectl patch pvc data-postgres-0 -n database \
  -p '{"spec": {"resources": {"requests": {"storage": "200Gi"}}}}'

# 확장 상태 확인
kubectl get pvc data-postgres-0 -n database -o yaml | grep -A 5 status

4. Headless Service와 DNS

4.1 Headless Service 정의

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: database
spec:
  clusterIP: None  # Headless Service의 핵심
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
      name: postgresql

4.2 DNS 규칙

StatefulSet의 각 Pod는 예측 가능한 DNS 이름을 갖습니다.

DNS 패턴:
  pod-name.service-name.namespace.svc.cluster.local

예시:
  postgres-0.postgres-headless.database.svc.cluster.local
  postgres-1.postgres-headless.database.svc.cluster.local
  postgres-2.postgres-headless.database.svc.cluster.local
# 읽기/쓰기 분리를 위한 추가 Service
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-primary
  namespace: database
spec:
  selector:
    app: postgres
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-replica
  namespace: database
spec:
  selector:
    app: postgres
    role: replica
  ports:
    - port: 5432
      targetPort: 5432

5. Database Operator

5.1 왜 Operator가 필요한가?

DB 운영은 단순한 배포를 넘어 복잡한 Day-2 운영이 필요합니다.

Operator가 자동화하는 작업:

1. 클러스터 초기화 (Primary + Replica 구성)
2. 자동 Failover (Primary 장애 시 Replica 승격)
3. 백업/복구 (스케줄링, PITR)
4. 롤링 업그레이드 (무중단)
5. 수평 확장 (Replica 추가/제거)
6. 모니터링 통합
7. 인증서 관리 (TLS)
8. 설정 변경 (재시작 없이)

5.2 PostgreSQL - CloudNativePG (CNPG)

# CloudNativePG 설치
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml
# CloudNativePG 클러스터 정의
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
  namespace: database
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "1GB"
      effective_cache_size: "3GB"
      work_mem: "16MB"
      maintenance_work_mem: "256MB"
      wal_buffers: "16MB"
      random_page_cost: "1.1"
      effective_io_concurrency: "200"
      max_wal_size: "2GB"
      checkpoint_completion_target: "0.9"

  bootstrap:
    initdb:
      database: myapp
      owner: app_user
      secret:
        name: myapp-db-credentials

  storage:
    size: 100Gi
    storageClass: fast-ssd

  resources:
    requests:
      memory: "2Gi"
      cpu: "1"
    limits:
      memory: "4Gi"
      cpu: "2"

  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
      data:
        compression: gzip
    retentionPolicy: "30d"

  monitoring:
    enablePodMonitor: true

5.3 MySQL - Percona XtraDB Cluster Operator

# Percona Operator 설치
kubectl apply -f https://raw.githubusercontent.com/percona/percona-xtradb-cluster-operator/v1.15.0/deploy/bundle.yaml
# Percona XtraDB Cluster 정의
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
  name: myapp-mysql
  namespace: database
spec:
  crVersion: "1.15.0"
  secretsName: myapp-mysql-secrets

  pxc:
    size: 3
    image: percona/percona-xtradb-cluster:8.0.36
    resources:
      requests:
        memory: 2G
        cpu: "1"
      limits:
        memory: 4G
        cpu: "2"
    volumeSpec:
      persistentVolumeClaim:
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi
    affinity:
      antiAffinityTopologyKey: "kubernetes.io/hostname"

  haproxy:
    enabled: true
    size: 3
    image: percona/haproxy:2.8.5
    resources:
      requests:
        memory: 512M
        cpu: "500m"

  backup:
    image: percona/percona-xtradb-cluster-operator:1.15.0-pxc8.0-backup
    storages:
      s3-backup:
        type: s3
        s3:
          bucket: my-backup-bucket
          credentialsSecret: aws-creds
          region: ap-northeast-2
    schedule:
      - name: daily-backup
        schedule: "0 3 * * *"
        keep: 7
        storageName: s3-backup

5.4 MongoDB - Community Operator

# MongoDB Community Operator 설치
kubectl apply -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml
kubectl apply -k https://github.com/mongodb/mongodb-kubernetes-operator/config/rbac/
kubectl create -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/manager/manager.yaml
# MongoDB ReplicaSet 정의
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: myapp-mongodb
  namespace: database
spec:
  members: 3
  type: ReplicaSet
  version: "7.0.14"

  security:
    authentication:
      modes: ["SCRAM"]

  users:
    - name: app-user
      db: admin
      passwordSecretRef:
        name: mongodb-password
      roles:
        - name: readWrite
          db: myapp
        - name: clusterAdmin
          db: admin
      scramCredentialsSecretName: app-user-scram

  statefulSet:
    spec:
      template:
        spec:
          containers:
            - name: mongod
              resources:
                requests:
                  cpu: "1"
                  memory: 2Gi
                limits:
                  cpu: "2"
                  memory: 4Gi
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 100Gi

5.5 Operator 비교

특성CloudNativePGPercona XtraDBMongoDB Community
DBPostgreSQLMySQLMongoDB
라이선스Apache 2.0Apache 2.0SSPL + Apache
HA 방식Streaming ReplicationGalera ClusterReplicaSet
자동 Failover지원지원지원
백업Barman/S3xtrabackup/S3mongodump 연동
모니터링PodMonitorPMM 통합기본 메트릭
성숙도매우 높음높음중간

6. 고가용성 (HA)

6.1 Primary-Replica 구성

PostgreSQL HA 아키텍처 (CloudNativePG):

  [CNPG Operator]
       |
       v
  [Primary Pod]  ----Streaming Replication---->  [Replica Pod 1]
       |                                          [Replica Pod 2]
       |
  [Headless Service]
       |
  postgres-primary (쓰기) ----> Primary만 라우팅
  postgres-replica (읽기) ----> Replica만 라우팅

6.2 자동 Failover

Failover 시나리오:

1. Primary Pod 장애 발생
2. Operator가 감지 (liveness probe 실패)
3. 가장 최신 LSN을 가진 Replica 선택
4. 선택된 Replica를 Primary로 승격
5. 나머지 Replica가Primary를 추종
6. Service 엔드포인트 업데이트
7. 장애 Pod 재생성 후 새 Replica로 합류

전체 과정: 보통 30초 이내

6.3 PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: database
spec:
  minAvailable: 2  # 최소 2개 Pod 유지
  selector:
    matchLabels:
      app: postgres

6.4 노드 장애 대비

# Pod Topology Spread Constraints
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres

7. 백업과 복구

7.1 백업 유형

논리적 백업 (pg_dump/mysqldump):
  + 이식성 높음 (다른 버전/플랫폼으로 복원 가능)
  + 개별 테이블/스키마 백업 가능
  - 대용량 DB에서 느림
  - 복원 시 인덱스 재생성 필요

물리적 백업 (pgBackRest/xtrabackup):
  + 대용량 DB에서 빠름
  + 증분 백업 지원
  + PITR(Point-in-Time Recovery) 가능
  - 같은 메이저 버전에서만 복원
  - 전체 클러스터 단위 백업

7.2 pgBackRest (PostgreSQL)

# CloudNativePG의 백업 설정
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
spec:
  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
        maxParallel: 4
      data:
        compression: gzip
        immediateCheckpoint: true
    retentionPolicy: "30d"
---
# 스케줄 백업
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: myapp-db-daily
spec:
  schedule: "0 3 * * *"
  cluster:
    name: myapp-db
  backupOwnerReference: self
  method: barmanObjectStore

7.3 PITR (Point-in-Time Recovery)

# PITR로 특정 시점 복구
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db-recovered
spec:
  instances: 3

  bootstrap:
    recovery:
      source: myapp-db-backup
      recoveryTarget:
        targetTime: "2026-03-24T10:30:00Z"  # 복구 시점

  externalClusters:
    - name: myapp-db-backup
      barmanObjectStore:
        destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
        s3Credentials:
          accessKeyId:
            name: aws-creds
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: aws-creds
            key: SECRET_ACCESS_KEY

7.4 Velero를 활용한 전체 백업

# Velero 설치
velero install \
  --provider aws \
  --bucket my-velero-bucket \
  --secret-file ./credentials-velero \
  --plugins velero/velero-plugin-for-aws:v1.10.0

# 네임스페이스 단위 백업
velero backup create database-backup \
  --include-namespaces database \
  --snapshot-volumes=true \
  --volume-snapshot-locations default

# 복구
velero restore create --from-backup database-backup

8. 모니터링

8.1 Prometheus + Grafana

# PostgreSQL Exporter (pg_exporter)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-exporter
  namespace: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-exporter
  template:
    metadata:
      labels:
        app: postgres-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9187"
    spec:
      containers:
        - name: exporter
          image: prometheuscommunity/postgres-exporter:0.15.0
          ports:
            - containerPort: 9187
          env:
            - name: DATA_SOURCE_URI
              value: "postgres-primary.database.svc:5432/myapp?sslmode=disable"
            - name: DATA_SOURCE_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: DATA_SOURCE_PASS
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

8.2 주요 모니터링 지표

DB 모니터링 핵심 메트릭:

성능:
  - 쿼리 수/ (QPS)
  - 쿼리 레이턴시 (p50, p95, p99)
  - 활성 커넥션 수
  - 캐시 히트율 (Buffer Cache Hit Ratio)

복제:
  - 복제 지연 (Replication Lag)
  - WAL 수신 지연
  - Replica 상태

스토리지:
  - 디스크 사용량
  - IOPS / 처리량
  - WAL 크기

리소스:
  - CPU 사용률
  - 메모리 사용량
  - Pod 재시작 횟수

운영:
  - Dead tuples 비율
  - Vacuum 실행 상태
  - 락 대기 수

8.3 Percona Monitoring and Management (PMM)

# PMM Server 설치
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pmm-server
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pmm-server
  template:
    spec:
      containers:
        - name: pmm-server
          image: percona/pmm-server:2
          ports:
            - containerPort: 443
          volumeMounts:
            - name: pmm-data
              mountPath: /srv
      volumes:
        - name: pmm-data
          persistentVolumeClaim:
            claimName: pmm-data

9. 성능 튜닝

9.1 리소스 Requests/Limits

# DB Pod 리소스 설정 가이드
resources:
  requests:
    # CPU: 보장받을 최소 CPU
    # DB는 CPU 경합에 민감하므로 넉넉히
    cpu: "2"
    # Memory: shared_buffers + work_mem * max_connections + OS
    memory: "4Gi"
  limits:
    # CPU limit은 설정하지 않거나 여유있게
    # (throttling이 쿼리 지연 유발)
    cpu: "4"
    # Memory limit은 OOM Kill 방지를 위해 request보다 여유있게
    memory: "8Gi"

9.2 Affinity와 Anti-Affinity

# DB Pod 스케줄링 최적화
spec:
  template:
    spec:
      # DB 전용 노드에만 스케줄링
      nodeSelector:
        node-type: database

      # 또는 Toleration으로 전용 노드
      tolerations:
        - key: "dedicated"
          operator: "Equal"
          value: "database"
          effect: "NoSchedule"

      # Replica를 다른 노드/존에 분배
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - postgres
              topologyKey: "kubernetes.io/hostname"

9.3 커널 파라미터 튜닝

# initContainer로 커널 파라미터 설정
spec:
  template:
    spec:
      initContainers:
        - name: sysctl-tuning
          image: busybox:1.36
          securityContext:
            privileged: true
          command:
            - sh
            - -c
            - |
              sysctl -w vm.swappiness=1
              sysctl -w vm.dirty_background_ratio=5
              sysctl -w vm.dirty_ratio=10
              sysctl -w vm.overcommit_memory=2
              sysctl -w net.core.somaxconn=65535
              sysctl -w net.ipv4.tcp_max_syn_backlog=65535

10. 보안

10.1 NetworkPolicy

# DB Pod에 대한 네트워크 접근 제한
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-network-policy
  namespace: database
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: application
          podSelector:
            matchLabels:
              app: backend
        - podSelector:
            matchLabels:
              app: postgres  # Pod 간 복제 허용
      ports:
        - port: 5432
          protocol: TCP
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - port: 5432
          protocol: TCP
    - to:  # DNS 허용
        - namespaceSelector: {}
      ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP

10.2 Secrets 관리

# External Secrets Operator 사용
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: postgres-secret
  namespace: database
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: postgres-secret
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/postgres
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/postgres
        property: password

10.3 TLS 설정

# cert-manager로 DB 인증서 발급
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: postgres-tls
  namespace: database
spec:
  secretName: postgres-tls-secret
  duration: 8760h  # 1년
  renewBefore: 720h  # 30일 전 갱신
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  dnsNames:
    - postgres-primary.database.svc.cluster.local
    - postgres-headless.database.svc.cluster.local
    - "*.postgres-headless.database.svc.cluster.local"

11. VM에서 K8s로 마이그레이션

11.1 마이그레이션 단계

Phase 1: 준비
  1. K8s 클러스터에 DB Operator 설치
  2. StorageClass, NetworkPolicy 구성
  3. 타겟 DB 클러스터 생성 (빈 상태)

Phase 2: 데이터 마이그레이션
  방법 A - 논리적 복제 (최소 다운타임):
    1. VM DB에서 논리적 복제(Logical Replication) 설정
    2. K8s DBSubscriber로 설정
    3. 초기 동기화 + 변경분 스트리밍
    4. 애플리케이션 전환 (짧은 다운타임)

  방법 B - pg_dump/restore:
    1. VM DB 백업 (pg_dump)
    2. K8s DB복원 (pg_restore)
    3. 다운타임 동안 전환

Phase 3: 전환
  1. 애플리케이션 DB 연결 문자열 변경
  2. DNS 변경 또는 Service 엔드포인트 업데이트
  3. 이전 VM DB를 읽기 전용으로 전환 (롤백 대비)

Phase 4: 정리
  1. 검증 완료 후 이전 VM DB 삭제
  2. 모니터링/알림 업데이트

11.2 논리적 복제 설정

-- VM DB (Publisher) 설정
-- postgresql.conf
-- wal_level = logical
-- max_replication_slots = 10

CREATE PUBLICATION myapp_pub FOR ALL TABLES;

-- K8s DB (Subscriber) 설정
CREATE SUBSCRIPTION myapp_sub
  CONNECTION 'host=vm-db.example.com port=5432 dbname=myapp user=repl_user password=secret'
  PUBLICATION myapp_pub;

12. 프로덕션 체크리스트

K8s DB 프로덕션 체크리스트:

스토리지:
  [ ] StorageClass에 reclaimPolicy: Retain 설정
  [ ] 볼륨 크기에 20% 이상 여유 공간
  [ ] allowVolumeExpansion: true 확인
  [ ] IOPS/throughput이 워크로드에 적합한지 확인

고가용성:
  [ ] 최소 3인스턴스 (1 Primary + 2 Replica)
  [ ] PodDisruptionBudget 설정
  [ ] Pod Anti-Affinity (다른 노드/존에 분배)
  [ ] 자동 Failover 테스트 완료

백업:
  [ ] 자동 백업 스케줄 설정 (1회 이상)
  [ ] WAL 아카이빙 활성화 (PITR 지원)
  [ ] 백업 복구 테스트 완료
  [ ] 백업 보관 정책 설정 (30일 이상)

보안:
  [ ] NetworkPolicy로 접근 제한
  [ ] Secrets는 External Secrets Operator로 관리
  [ ] TLS 암호화 활성화
  [ ] DB 사용자 권한 최소화 (최소 권한 원칙)

모니터링:
  [ ] Prometheus + Grafana 대시보드 구성
  [ ] 알림 규칙 설정 (복제 지연, 디스크 사용률, 커넥션 수)
  [ ] 로그 수집 (Loki/EFK)
  [ ] 쿼리 성능 모니터링

성능:
  [ ] Resource requests/limits 적절히 설정
  [ ] DB 파라미터 튜닝 (shared_buffers, work_mem 등)
  [ ] 커널 파라미터 최적화
  [ ] 전용 노드 사용 (Taint/Toleration)

13. 실전 퀴즈

Q1: StatefulSet과 Deployment의 핵심 차이는 무엇인가요?

정답:

StatefulSet은 다음을 보장합니다:

  1. 안정적인 네트워크 ID: 각 Pod에 순차적인 이름이 부여됩니다 (예: db-0, db-1, db-2). Pod가 재생성되어도 같은 이름을 유지합니다.
  2. 영구 스토리지 바인딩: volumeClaimTemplates로 각 Pod에 전용 PVC가 자동 생성됩니다. Pod가 삭제/재생성되어도 같은 PVC에 다시 연결됩니다.
  3. 순차적 생성/삭제: Pod 0이 Ready 상태가 된 후에 Pod 1이 생성됩니다 (OrderedReady 정책).

반면 Deployment는 임의의 Pod 이름, 공유 볼륨, 병렬 생성을 사용하므로 Stateless 앱에 적합합니다.

Q2: DB 스토리지의 reclaimPolicy를 Retain으로 설정해야 하는 이유는?

정답:

reclaimPolicy가 Delete(기본값)이면 PVC가 삭제될 때 PV와 실제 스토리지(EBS 볼륨 등)도 함께 삭제됩니다. 이는 다음 상황에서 데이터 손실을 유발합니다:

  • 실수로 StatefulSet이나 PVC를 삭제
  • 네임스페이스 삭제
  • Helm uninstall

Retain으로 설정하면 PVC가 삭제되어도 PV와 실제 스토리지가 유지되어, 데이터 복구가 가능합니다. 프로덕션 DB에서는 반드시 Retain을 사용해야 합니다.

Q3: CloudNativePG에서 자동 Failover는 어떻게 동작하나요?

정답:

  1. CNPG Operator가 모든 인스턴스의 상태를 지속적으로 모니터링합니다.
  2. Primary Pod의 liveness probe가 실패하면, Operator가 장애를 감지합니다.
  3. Operator는 모든 Replica의 WAL LSN(Log Sequence Number)을 비교하여 가장 최신 데이터를 가진 Replica를 선택합니다.
  4. 선택된 Replica가 Primary로 승격되고, pg_promote가 실행됩니다.
  5. 나머지 Replica는 새 Primary를 추종하도록 재설정됩니다.
  6. Service 엔드포인트가 새 Primary를 가리키도록 자동 업데이트됩니다.
  7. 장애 Pod는 재생성되어 새 Replica로 합류합니다.

전체 과정은 일반적으로 30초 이내에 완료됩니다.

Q4: K8s에서 DB 성능을 위해 CPU limit을 설정하지 않는 것이 왜 권장되나요?

정답:

CPU limit이 설정되면 Kubernetes는 CFS (Completely Fair Scheduler) throttling을 적용합니다. DB가 순간적으로 높은 CPU를 필요로 할 때 (예: 복잡한 쿼리, VACUUM) throttling이 발생하면 쿼리 레이턴시가 크게 증가합니다.

대신 다음 전략을 권장합니다:

  • CPU request만 설정하여 보장된 CPU를 확보
  • DB 전용 노드를 사용 (Taint/Toleration)하여 다른 워크로드와 리소스 경합을 방지
  • 노드 수준에서 CPU 리소스가 충분한지 확인

Memory limit은 OOM Kill을 방지하기 위해 설정하되, request보다 충분히 여유있게 설정합니다.

Q5: DB를 VM에서 K8s로 마이그레이션할 때 최소 다운타임을 위한 전략은?

정답:

논리적 복제(Logical Replication)를 활용합니다:

  1. VM DB에서 wal_level = logical을 설정하고 Publication을 생성합니다.
  2. K8s DB에서 Subscription을 생성하여 VM DB에 연결합니다.
  3. 초기 데이터 동기화가 자동으로 진행됩니다.
  4. 동기화 완료 후, 변경분이 실시간으로 스트리밍됩니다.
  5. 애플리케이션을 잠시 중단(수초~수분)하고, 복제 지연이 0인지 확인합니다.
  6. 애플리케이션의 DB 연결 문자열을 K8s DB로 변경합니다.
  7. 애플리케이션을 재시작합니다.

이 방법으로 다운타임을 수초~수분으로 최소화할 수 있습니다.


14. 참고 자료

  1. CloudNativePG Documentation
  2. Percona Operator for MySQL Documentation
  3. MongoDB Kubernetes Operator
  4. Kubernetes StatefulSet Documentation
  5. Kubernetes Persistent Volumes
  6. Velero - Backup and Restore
  7. External Secrets Operator
  8. Percona Monitoring and Management (PMM)
  9. PostgreSQL Kubernetes Best Practices
  10. Zalando Postgres Operator
  11. CrunchyData PGO
  12. K8ssandra - Cassandra on Kubernetes
  13. Data on Kubernetes Community
  14. CNCF Storage Landscape

Running Databases on Kubernetes Complete Guide: StatefulSet, Operators, Backup and Recovery

TL;DR

  • StatefulSet: K8s workload for stateful applications like databases (stable network IDs + persistent storage)
  • DB Operators: Automate operations with CloudNativePG (PostgreSQL), Percona Operator (MySQL/MongoDB)
  • Storage: Manage persistent data with PV/PVC/StorageClass, leverage CSI drivers
  • High Availability: Primary-Replica configuration, automatic failover, PodDisruptionBudget
  • Backup/Recovery: pgBackRest, Velero, PITR (Point-in-Time Recovery) strategies
  • Monitoring: PMM, pg_exporter + Prometheus + Grafana dashboards

Table of Contents

  1. Should You Run DBs on K8s?
  2. StatefulSet Deep Dive
  3. Storage Strategy
  4. Headless Service and DNS
  5. Database Operators
  6. High Availability (HA)
  7. Backup and Recovery
  8. Monitoring
  9. Performance Tuning
  10. Security
  11. Migrating from VMs to K8s
  12. Production Checklist
  13. Practical Quiz
  14. References

1. Should You Run DBs on K8s?

1.1 Pros and Cons

K8s DB operations decision matrix:

When you should:                        When you should not:
+ Multi-cloud/hybrid environments       - Managed DB service available
+ Infrastructure consistency matters    - Lack of DBA resources
+ GitOps/IaC pipeline integration       - Very large single DB instances
+ Dev/test environment automation       - Extremely low latency required
+ Cost optimization is essential        - Insufficient K8s experience
+ Data sovereignty regulations          - Simple architecture is enough
CriteriaK8s DB OperationsManaged Service (RDS/CloudSQL)
Initial setup complexityHighLow
Operations automationPossible via OperatorsBuilt-in
CostEfficient (resource sharing)Premium pricing
Multi-cloudEasyVendor lock-in
CustomizationFull freedomLimited
Backup/RecoveryManual configurationBuilt-in
ScalabilityManual/semi-automaticAuto-scaling

1.2 Which DBs Are Suitable for K8s?

K8s compatibility ranking:

Highly suitable:
  - PostgreSQL (excellent CloudNativePG ecosystem)
  - MongoDB (ReplicaSet structure fits K8s naturally)
  - Redis (Sentinel/Cluster mode)

Suitable:
  - MySQL (Percona/Oracle Operator available)
  - Elasticsearch (ECK Operator)
  - Cassandra (K8ssandra Operator)

Use with caution:
  - Oracle DB (licensing, complexity)
  - SQL Server (Windows container limitations)
  - Large single-instance DBs

2. StatefulSet Deep Dive

2.1 StatefulSet vs Deployment

Deployment:                    StatefulSet:
- Pod names: random (abc-xyz)  - Pod names: sequential (db-0, db-1, db-2)
- Parallel create/delete       - Sequential create/delete (0 -> 1 -> 2)
- Shared volume or none        - Dedicated PVC per Pod (auto-created)
- Interchangeable Pods         - Pods with unique identity
- Suited for stateless apps    - Suited for stateful apps (DB)

2.2 StatefulSet YAML Example

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres-headless  # Headless Service name
  replicas: 3
  podManagementPolicy: OrderedReady  # Sequential creation (default)
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1  # K8s 1.24+
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      terminationGracePeriodSeconds: 120  # Enough time for DB shutdown
      securityContext:
        fsGroup: 999       # postgres group
        runAsUser: 999     # postgres user
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
              name: postgresql
          env:
            - name: POSTGRES_DB
              value: myapp
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

2.3 PodManagementPolicy

# OrderedReady (default): Sequential create/delete
# Pod 0 Ready -> Pod 1 created -> Pod 1 Ready -> Pod 2 created
podManagementPolicy: OrderedReady

# Parallel: All Pods created/deleted simultaneously
# Be careful with initial bootstrapping (DBs typically use OrderedReady)
podManagementPolicy: Parallel

2.4 UpdateStrategy

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    # Partition: Only Pods with ordinal at or above this value are updated
    # Useful for canary deployments (update only Pod 2 first)
    partition: 2

# OnDelete: Updates only when Pods are manually deleted
# Provides fine-grained control during DB upgrades
updateStrategy:
  type: OnDelete

3. Storage Strategy

3.1 PV / PVC / StorageClass

# StorageClass definition (AWS EBS gp3)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "5000"
  throughput: "250"    # MB/s
  encrypted: "true"
reclaimPolicy: Retain  # MUST be Retain for DB data!
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true  # Allow online volume expansion
# StorageClass definition (GCP PD SSD)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
  replication-type: regional-pd  # Regional PD (high availability)
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

3.2 Local Storage vs Cloud Volumes

Performance comparison:

Local NVMe SSD:
  - Random reads: 500K+ IOPS
  - Latency: under 0.1ms
  - Drawback: No Pod migration, data loss risk on node failure

Cloud EBS gp3:
  - Baseline: 3,000 IOPS / 125 MB/s
  - Maximum: 16,000 IOPS / 1,000 MB/s
  - Advantage: Pod migration possible, snapshot support

Cloud EBS io2:
  - Maximum: 64,000 IOPS
  - 99.999% durability
  - Expensive but suitable for mission-critical DBs

3.3 Volume Expansion

# Expand PVC size (requires allowVolumeExpansion: true on StorageClass)
kubectl patch pvc data-postgres-0 -n database \
  -p '{"spec": {"resources": {"requests": {"storage": "200Gi"}}}}'

# Check expansion status
kubectl get pvc data-postgres-0 -n database -o yaml | grep -A 5 status

4. Headless Service and DNS

4.1 Headless Service Definition

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: database
spec:
  clusterIP: None  # The key to Headless Service
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
      name: postgresql

4.2 DNS Rules

Each Pod in a StatefulSet gets a predictable DNS name.

DNS pattern:
  pod-name.service-name.namespace.svc.cluster.local

Examples:
  postgres-0.postgres-headless.database.svc.cluster.local
  postgres-1.postgres-headless.database.svc.cluster.local
  postgres-2.postgres-headless.database.svc.cluster.local
# Additional Services for read/write splitting
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-primary
  namespace: database
spec:
  selector:
    app: postgres
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-replica
  namespace: database
spec:
  selector:
    app: postgres
    role: replica
  ports:
    - port: 5432
      targetPort: 5432

5. Database Operators

5.1 Why Are Operators Needed?

DB operations require complex Day-2 operations beyond simple deployment.

Tasks automated by Operators:

1. Cluster initialization (Primary + Replica setup)
2. Automatic failover (Replica promotion on Primary failure)
3. Backup/Recovery (scheduling, PITR)
4. Rolling upgrades (zero downtime)
5. Horizontal scaling (add/remove Replicas)
6. Monitoring integration
7. Certificate management (TLS)
8. Configuration changes (without restart)

5.2 PostgreSQL - CloudNativePG (CNPG)

# Install CloudNativePG
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml
# CloudNativePG Cluster definition
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
  namespace: database
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "1GB"
      effective_cache_size: "3GB"
      work_mem: "16MB"
      maintenance_work_mem: "256MB"
      wal_buffers: "16MB"
      random_page_cost: "1.1"
      effective_io_concurrency: "200"
      max_wal_size: "2GB"
      checkpoint_completion_target: "0.9"

  bootstrap:
    initdb:
      database: myapp
      owner: app_user
      secret:
        name: myapp-db-credentials

  storage:
    size: 100Gi
    storageClass: fast-ssd

  resources:
    requests:
      memory: "2Gi"
      cpu: "1"
    limits:
      memory: "4Gi"
      cpu: "2"

  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
      data:
        compression: gzip
    retentionPolicy: "30d"

  monitoring:
    enablePodMonitor: true

5.3 MySQL - Percona XtraDB Cluster Operator

# Install Percona Operator
kubectl apply -f https://raw.githubusercontent.com/percona/percona-xtradb-cluster-operator/v1.15.0/deploy/bundle.yaml
# Percona XtraDB Cluster definition
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
  name: myapp-mysql
  namespace: database
spec:
  crVersion: "1.15.0"
  secretsName: myapp-mysql-secrets

  pxc:
    size: 3
    image: percona/percona-xtradb-cluster:8.0.36
    resources:
      requests:
        memory: 2G
        cpu: "1"
      limits:
        memory: 4G
        cpu: "2"
    volumeSpec:
      persistentVolumeClaim:
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi
    affinity:
      antiAffinityTopologyKey: "kubernetes.io/hostname"

  haproxy:
    enabled: true
    size: 3
    image: percona/haproxy:2.8.5
    resources:
      requests:
        memory: 512M
        cpu: "500m"

  backup:
    image: percona/percona-xtradb-cluster-operator:1.15.0-pxc8.0-backup
    storages:
      s3-backup:
        type: s3
        s3:
          bucket: my-backup-bucket
          credentialsSecret: aws-creds
          region: ap-northeast-2
    schedule:
      - name: daily-backup
        schedule: "0 3 * * *"
        keep: 7
        storageName: s3-backup

5.4 MongoDB - Community Operator

# Install MongoDB Community Operator
kubectl apply -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml
kubectl apply -k https://github.com/mongodb/mongodb-kubernetes-operator/config/rbac/
kubectl create -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/manager/manager.yaml
# MongoDB ReplicaSet definition
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: myapp-mongodb
  namespace: database
spec:
  members: 3
  type: ReplicaSet
  version: "7.0.14"

  security:
    authentication:
      modes: ["SCRAM"]

  users:
    - name: app-user
      db: admin
      passwordSecretRef:
        name: mongodb-password
      roles:
        - name: readWrite
          db: myapp
        - name: clusterAdmin
          db: admin
      scramCredentialsSecretName: app-user-scram

  statefulSet:
    spec:
      template:
        spec:
          containers:
            - name: mongod
              resources:
                requests:
                  cpu: "1"
                  memory: 2Gi
                limits:
                  cpu: "2"
                  memory: 4Gi
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 100Gi

5.5 Operator Comparison

FeatureCloudNativePGPercona XtraDBMongoDB Community
DatabasePostgreSQLMySQLMongoDB
LicenseApache 2.0Apache 2.0SSPL + Apache
HA MethodStreaming ReplicationGalera ClusterReplicaSet
Auto FailoverSupportedSupportedSupported
BackupBarman/S3xtrabackup/S3mongodump integration
MonitoringPodMonitorPMM integrationBasic metrics
MaturityVery highHighMedium

6. High Availability (HA)

6.1 Primary-Replica Configuration

PostgreSQL HA architecture (CloudNativePG):

  [CNPG Operator]
       |
       v
  [Primary Pod]  ----Streaming Replication---->  [Replica Pod 1]
       |                                          [Replica Pod 2]
       |
  [Headless Service]
       |
  postgres-primary (writes) ----> Routes to Primary only
  postgres-replica (reads)  ----> Routes to Replicas only

6.2 Automatic Failover

Failover scenario:

1. Primary Pod failure occurs
2. Operator detects it (liveness probe failure)
3. Selects Replica with most recent LSN
4. Promotes selected Replica to Primary
5. Remaining Replicas follow the new Primary
6. Service endpoints updated
7. Failed Pod recreated and joins as new Replica

Entire process: typically under 30 seconds

6.3 PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: database
spec:
  minAvailable: 2  # Maintain at least 2 Pods
  selector:
    matchLabels:
      app: postgres

6.4 Node Failure Protection

# Pod Topology Spread Constraints
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres

7. Backup and Recovery

7.1 Backup Types

Logical backup (pg_dump/mysqldump):
  + High portability (restore to different versions/platforms)
  + Individual table/schema backup possible
  - Slow for large DBs
  - Index rebuilding needed on restore

Physical backup (pgBackRest/xtrabackup):
  + Fast for large DBs
  + Incremental backup support
  + PITR (Point-in-Time Recovery) possible
  - Restore only within same major version
  - Whole cluster backup only

7.2 pgBackRest (PostgreSQL)

# CloudNativePG backup configuration
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
spec:
  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
        maxParallel: 4
      data:
        compression: gzip
        immediateCheckpoint: true
    retentionPolicy: "30d"
---
# Scheduled backup
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: myapp-db-daily
spec:
  schedule: "0 3 * * *"
  cluster:
    name: myapp-db
  backupOwnerReference: self
  method: barmanObjectStore

7.3 PITR (Point-in-Time Recovery)

# Recover to a specific point in time with PITR
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db-recovered
spec:
  instances: 3

  bootstrap:
    recovery:
      source: myapp-db-backup
      recoveryTarget:
        targetTime: "2026-03-24T10:30:00Z"  # Recovery target time

  externalClusters:
    - name: myapp-db-backup
      barmanObjectStore:
        destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
        s3Credentials:
          accessKeyId:
            name: aws-creds
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: aws-creds
            key: SECRET_ACCESS_KEY

7.4 Full Backup with Velero

# Install Velero
velero install \
  --provider aws \
  --bucket my-velero-bucket \
  --secret-file ./credentials-velero \
  --plugins velero/velero-plugin-for-aws:v1.10.0

# Namespace-level backup
velero backup create database-backup \
  --include-namespaces database \
  --snapshot-volumes=true \
  --volume-snapshot-locations default

# Restore
velero restore create --from-backup database-backup

8. Monitoring

8.1 Prometheus + Grafana

# PostgreSQL Exporter (pg_exporter)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-exporter
  namespace: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-exporter
  template:
    metadata:
      labels:
        app: postgres-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9187"
    spec:
      containers:
        - name: exporter
          image: prometheuscommunity/postgres-exporter:0.15.0
          ports:
            - containerPort: 9187
          env:
            - name: DATA_SOURCE_URI
              value: "postgres-primary.database.svc:5432/myapp?sslmode=disable"
            - name: DATA_SOURCE_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: DATA_SOURCE_PASS
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

8.2 Key Monitoring Metrics

DB monitoring essential metrics:

Performance:
  - Queries per second (QPS)
  - Query latency (p50, p95, p99)
  - Active connection count
  - Cache hit ratio (Buffer Cache Hit Ratio)

Replication:
  - Replication lag
  - WAL receive delay
  - Replica status

Storage:
  - Disk usage
  - IOPS / throughput
  - WAL size

Resources:
  - CPU utilization
  - Memory usage
  - Pod restart count

Operations:
  - Dead tuples ratio
  - Vacuum execution status
  - Lock wait count

8.3 Percona Monitoring and Management (PMM)

# PMM Server installation
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pmm-server
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pmm-server
  template:
    spec:
      containers:
        - name: pmm-server
          image: percona/pmm-server:2
          ports:
            - containerPort: 443
          volumeMounts:
            - name: pmm-data
              mountPath: /srv
      volumes:
        - name: pmm-data
          persistentVolumeClaim:
            claimName: pmm-data

9. Performance Tuning

9.1 Resource Requests/Limits

# DB Pod resource configuration guide
resources:
  requests:
    # CPU: Minimum guaranteed CPU
    # DBs are sensitive to CPU contention, so be generous
    cpu: "2"
    # Memory: shared_buffers + work_mem * max_connections + OS
    memory: "4Gi"
  limits:
    # CPU limit: Don't set or set generously
    # (throttling causes query latency)
    cpu: "4"
    # Memory limit: Set higher than request to prevent OOM Kill
    memory: "8Gi"

9.2 Affinity and Anti-Affinity

# DB Pod scheduling optimization
spec:
  template:
    spec:
      # Schedule only on DB-dedicated nodes
      nodeSelector:
        node-type: database

      # Or use Toleration for dedicated nodes
      tolerations:
        - key: "dedicated"
          operator: "Equal"
          value: "database"
          effect: "NoSchedule"

      # Distribute Replicas across different nodes/zones
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - postgres
              topologyKey: "kubernetes.io/hostname"

9.3 Kernel Parameter Tuning

# Set kernel parameters via initContainer
spec:
  template:
    spec:
      initContainers:
        - name: sysctl-tuning
          image: busybox:1.36
          securityContext:
            privileged: true
          command:
            - sh
            - -c
            - |
              sysctl -w vm.swappiness=1
              sysctl -w vm.dirty_background_ratio=5
              sysctl -w vm.dirty_ratio=10
              sysctl -w vm.overcommit_memory=2
              sysctl -w net.core.somaxconn=65535
              sysctl -w net.ipv4.tcp_max_syn_backlog=65535

10. Security

10.1 NetworkPolicy

# Restrict network access to DB Pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-network-policy
  namespace: database
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: application
          podSelector:
            matchLabels:
              app: backend
        - podSelector:
            matchLabels:
              app: postgres  # Allow inter-Pod replication
      ports:
        - port: 5432
          protocol: TCP
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - port: 5432
          protocol: TCP
    - to:  # Allow DNS
        - namespaceSelector: {}
      ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP

10.2 Secrets Management

# Using External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: postgres-secret
  namespace: database
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: postgres-secret
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/postgres
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/postgres
        property: password

10.3 TLS Configuration

# Issue DB certificates with cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: postgres-tls
  namespace: database
spec:
  secretName: postgres-tls-secret
  duration: 8760h  # 1 year
  renewBefore: 720h  # Renew 30 days before expiry
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  dnsNames:
    - postgres-primary.database.svc.cluster.local
    - postgres-headless.database.svc.cluster.local
    - "*.postgres-headless.database.svc.cluster.local"

11. Migrating from VMs to K8s

11.1 Migration Phases

Phase 1: Preparation
  1. Install DB Operator on K8s cluster
  2. Configure StorageClass, NetworkPolicy
  3. Create target DB cluster (empty)

Phase 2: Data migration
  Method A - Logical replication (minimal downtime):
    1. Set up logical replication on VM DB
    2. Configure K8s DB as Subscriber
    3. Initial sync + change streaming
    4. Application switchover (brief downtime)

  Method B - pg_dump/restore:
    1. Backup VM DB (pg_dump)
    2. Restore to K8s DB (pg_restore)
    3. Switch during downtime window

Phase 3: Switchover
  1. Update application DB connection strings
  2. DNS change or Service endpoint update
  3. Set old VM DB to read-only (rollback safety)

Phase 4: Cleanup
  1. Delete old VM DB after verification
  2. Update monitoring/alerts

11.2 Logical Replication Setup

-- VM DB (Publisher) setup
-- postgresql.conf
-- wal_level = logical
-- max_replication_slots = 10

CREATE PUBLICATION myapp_pub FOR ALL TABLES;

-- K8s DB (Subscriber) setup
CREATE SUBSCRIPTION myapp_sub
  CONNECTION 'host=vm-db.example.com port=5432 dbname=myapp user=repl_user password=secret'
  PUBLICATION myapp_pub;

12. Production Checklist

K8s DB Production Checklist:

Storage:
  [ ] StorageClass has reclaimPolicy: Retain
  [ ] Volume size has 20%+ free space
  [ ] allowVolumeExpansion: true confirmed
  [ ] IOPS/throughput matches workload

High Availability:
  [ ] Minimum 3 instances (1 Primary + 2 Replica)
  [ ] PodDisruptionBudget configured
  [ ] Pod Anti-Affinity (spread across nodes/zones)
  [ ] Automatic failover tested

Backup:
  [ ] Automated backup schedule set (at least daily)
  [ ] WAL archiving enabled (for PITR)
  [ ] Backup restore tested
  [ ] Retention policy configured (30+ days)

Security:
  [ ] NetworkPolicy restricts access
  [ ] Secrets managed via External Secrets Operator
  [ ] TLS encryption enabled
  [ ] DB user permissions minimized (least privilege)

Monitoring:
  [ ] Prometheus + Grafana dashboard configured
  [ ] Alert rules set (replication lag, disk usage, connections)
  [ ] Log collection (Loki/EFK)
  [ ] Query performance monitoring

Performance:
  [ ] Resource requests/limits properly set
  [ ] DB parameters tuned (shared_buffers, work_mem, etc.)
  [ ] Kernel parameters optimized
  [ ] Dedicated nodes used (Taint/Toleration)

13. Practical Quiz

Q1: What is the key difference between StatefulSet and Deployment?

Answer:

StatefulSet guarantees:

  1. Stable network identity: Each Pod gets a sequential name (e.g., db-0, db-1, db-2). The name persists even when a Pod is recreated.
  2. Persistent storage binding: volumeClaimTemplates automatically create a dedicated PVC for each Pod. Even if a Pod is deleted and recreated, it reconnects to the same PVC.
  3. Sequential creation/deletion: Pod 1 is created only after Pod 0 reaches the Ready state (OrderedReady policy).

In contrast, Deployment uses random Pod names, shared volumes, and parallel creation, making it suitable for stateless apps.

Q2: Why should DB storage have reclaimPolicy set to Retain?

Answer:

When reclaimPolicy is Delete (the default), the PV and actual storage (EBS volume, etc.) are deleted along with the PVC. This causes data loss in these scenarios:

  • Accidental deletion of StatefulSet or PVC
  • Namespace deletion
  • Helm uninstall

With Retain, the PV and actual storage persist even when the PVC is deleted, enabling data recovery. Production databases must always use Retain.

Q3: How does automatic failover work in CloudNativePG?

Answer:

  1. The CNPG Operator continuously monitors all instances.
  2. When the Primary Pod's liveness probe fails, the Operator detects the failure.
  3. The Operator compares WAL LSN (Log Sequence Number) across all Replicas and selects the one with the most recent data.
  4. The selected Replica is promoted to Primary using pg_promote.
  5. Remaining Replicas are reconfigured to follow the new Primary.
  6. Service endpoints are automatically updated to point to the new Primary.
  7. The failed Pod is recreated and joins as a new Replica.

The entire process typically completes within 30 seconds.

Q4: Why is it recommended not to set CPU limits for DB Pods on K8s?

Answer:

When CPU limits are set, Kubernetes applies CFS (Completely Fair Scheduler) throttling. When a DB momentarily needs high CPU (e.g., complex queries, VACUUM), throttling causes significant query latency spikes.

Instead, these strategies are recommended:

  • Set only CPU requests to guarantee a minimum CPU allocation
  • Use dedicated DB nodes (Taint/Toleration) to prevent resource contention with other workloads
  • Ensure sufficient CPU resources at the node level

Memory limits should still be set to prevent OOM Kill, but with generous headroom above the request value.

Q5: What is the strategy for minimal downtime when migrating a DB from VMs to K8s?

Answer:

Use logical replication:

  1. Set wal_level = logical on the VM DB and create a Publication.
  2. Create a Subscription on the K8s DB connecting to the VM DB.
  3. Initial data synchronization proceeds automatically.
  4. After sync completes, changes are streamed in real time.
  5. Briefly pause the application (seconds to minutes) and verify replication lag is zero.
  6. Update the application's DB connection string to point to the K8s DB.
  7. Restart the application.

This approach minimizes downtime to seconds or minutes.


14. References

  1. CloudNativePG Documentation
  2. Percona Operator for MySQL Documentation
  3. MongoDB Kubernetes Operator
  4. Kubernetes StatefulSet Documentation
  5. Kubernetes Persistent Volumes
  6. Velero - Backup and Restore
  7. External Secrets Operator
  8. Percona Monitoring and Management (PMM)
  9. PostgreSQL Kubernetes Best Practices
  10. Zalando Postgres Operator
  11. CrunchyData PGO
  12. K8ssandra - Cassandra on Kubernetes
  13. Data on Kubernetes Community
  14. CNCF Storage Landscape