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

- Name
- Youngju Kim
- @fjvbn20031
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 대시보드
목차
- K8s에서 DB를 운영해야 할까?
- StatefulSet 심화
- 스토리지 전략
- Headless Service와 DNS
- Database Operator
- 고가용성 (HA)
- 백업과 복구
- 모니터링
- 성능 튜닝
- 보안
- VM에서 K8s로 마이그레이션
- 프로덕션 체크리스트
- 실전 퀴즈
- 참고 자료
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 비교
| 특성 | CloudNativePG | Percona XtraDB | MongoDB Community |
|---|---|---|---|
| DB | PostgreSQL | MySQL | MongoDB |
| 라이선스 | Apache 2.0 | Apache 2.0 | SSPL + Apache |
| HA 방식 | Streaming Replication | Galera Cluster | ReplicaSet |
| 자동 Failover | 지원 | 지원 | 지원 |
| 백업 | Barman/S3 | xtrabackup/S3 | mongodump 연동 |
| 모니터링 | PodMonitor | PMM 통합 | 기본 메트릭 |
| 성숙도 | 매우 높음 | 높음 | 중간 |
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 DB를 Subscriber로 설정
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은 다음을 보장합니다:
- 안정적인 네트워크 ID: 각 Pod에 순차적인 이름이 부여됩니다 (예: db-0, db-1, db-2). Pod가 재생성되어도 같은 이름을 유지합니다.
- 영구 스토리지 바인딩: volumeClaimTemplates로 각 Pod에 전용 PVC가 자동 생성됩니다. Pod가 삭제/재생성되어도 같은 PVC에 다시 연결됩니다.
- 순차적 생성/삭제: 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는 어떻게 동작하나요?
정답:
- CNPG Operator가 모든 인스턴스의 상태를 지속적으로 모니터링합니다.
- Primary Pod의 liveness probe가 실패하면, Operator가 장애를 감지합니다.
- Operator는 모든 Replica의 WAL LSN(Log Sequence Number)을 비교하여 가장 최신 데이터를 가진 Replica를 선택합니다.
- 선택된 Replica가 Primary로 승격되고, pg_promote가 실행됩니다.
- 나머지 Replica는 새 Primary를 추종하도록 재설정됩니다.
- Service 엔드포인트가 새 Primary를 가리키도록 자동 업데이트됩니다.
- 장애 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)를 활용합니다:
- VM DB에서
wal_level = logical을 설정하고 Publication을 생성합니다. - K8s DB에서 Subscription을 생성하여 VM DB에 연결합니다.
- 초기 데이터 동기화가 자동으로 진행됩니다.
- 동기화 완료 후, 변경분이 실시간으로 스트리밍됩니다.
- 애플리케이션을 잠시 중단(수초~수분)하고, 복제 지연이 0인지 확인합니다.
- 애플리케이션의 DB 연결 문자열을 K8s DB로 변경합니다.
- 애플리케이션을 재시작합니다.
이 방법으로 다운타임을 수초~수분으로 최소화할 수 있습니다.
14. 참고 자료
- CloudNativePG Documentation
- Percona Operator for MySQL Documentation
- MongoDB Kubernetes Operator
- Kubernetes StatefulSet Documentation
- Kubernetes Persistent Volumes
- Velero - Backup and Restore
- External Secrets Operator
- Percona Monitoring and Management (PMM)
- PostgreSQL Kubernetes Best Practices
- Zalando Postgres Operator
- CrunchyData PGO
- K8ssandra - Cassandra on Kubernetes
- Data on Kubernetes Community
- CNCF Storage Landscape