[Kubernetes] K3s 환경에서 PostgreSQL Master-Replica HA 구성하기 (Helm + hostPath PV 완벽 가이드)
jay
최근 미니피씨에 K3s를 구축하고 서비스를 올리는 작업을 진행 중이다. DB는 가장 익숙한 PostgreSQL을 선택했는데, 단순히 파드 하나 띄우는 것(Standalone) 말고, 읽기 부하 분산과 가용성을 위해 Master-Replica(Primary-ReadReplica) 구조를 잡고 싶었다.
Cloud 환경이었다면 EBS나 블록 스토리지로 뚝딱했겠지만, 맨땅에 헤딩하는 온프레미스 환경이라 스토리지부터 Helm 설정까지 꽤나 삽질을 했다. 그 과정과 최종 설정을 기록해둔다.
🏗️ 목표 아키텍처
- Cluster: K3s (Single Node ‘jay’)
- Install Tool: Helm (bitnami/postgresql)
- Architecture:
- Primary (Write): 고정된
hostPath경로(/srv/k3s/data/postgresql/master)를 사용하여 데이터 영속성 보장. - Read Replicas (Read): HPA를 통해 부하에 따라 자동 스케일링(Scale-out). 스토리지는 K3s 기본
local-path를 통한 동적 프로비저닝 사용.
- Primary (Write): 고정된

1. Primary용 정적 PV/PVC 생성 (고정 경로 사용)
Primary DB는 가장 중요하므로, 관리가 용이한 특정 경로에 데이터를 저장하기 위해 hostPath를 이용한 PV를 수동으로 생성하고 바인딩했다.
file: postgresql-master-pv-pvc.yaml
# PV: 실제 서버의 경로와 매핑
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgresql-dev-master-pv
labels:
type: local
spec:
storageClassName: postgresql-dev-pv # PVC와 매핑될 키값
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/srv/k3s/data/postgresql/master" # 실제 데이터 저장 경로
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- jay # 노드 호스트네임 (kubectl get nodes로 확인)
---
# PVC: Helm 차트가 사용할 요청서
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-dev-master-pvc
spec:
storageClassName: postgresql-dev-pv # PV와 동일한 이름 사용
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
selector:
matchLabels:
type: local # PV의 라벨과 일치해야 함
2. Helm Values 설정 (dev-values.yaml)
Bitnami 차트를 커스터마이징 했다. Primary는 정적 PVC를 쓰고, Replica는 동적 프로비저닝 + HPA를 쓰는 하이브리드 구성이다. (HPA는 설정이 제대로 먹지 않았는데,, 그 과정이 2편에서 계속된다.)
file: dev-values.yaml
## Bitnami PostgreSQL Helm Chart Values
# [중요] 이거 안 쓰면 Replica 안 뜬다. (기본값이 standalone임)
architecture: replication
global:
defaultStorageClass: "local-path"
auth:
existingSecret: "postgresql-secret"
secretKeys:
adminPasswordKey: "postgres-password"
userPasswordKey: "password"
replicationPasswordKey: "replication-password"
database: "firebat"
username: "devdb"
primary:
name: "primary"
persistence:
enabled: true
existingClaim: "postgresql-dev-master-pvc" # 위에서 만든 정적 PVC 연결
volumeName: "data"
size: 5Gi
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "250m"
memory: "256Mi"
readReplicas:
autoscaling:
enabled: true
minReplicas: 1 # [주의] 0으로 설정 시 트래픽 발생하면 장애로 이어짐. 최소 1 유지.
maxReplicas: 3
targetCPUUtilizationPercentage: 80
persistence:
enabled: true
existingClaim: "" # 동적 할당을 위해 비워둠
storageClass: "local-path" # K3s 기본 StorageClass 사용
size: 5Gi
accessModes:
- ReadWriteOnce
volumePermissions:
enabled: true
containerSecurityContext:
runAsUser: 0
privileged: true
🚨 트러블 슈팅 (삽질 로그)
Helm 배포 과정에서 겪었던 주요 이슈들과 해결 방법이다.
Issue 1. “Replica 파드가 아예 안 뜸”
- 현상:
helm install을 했는데 Primary 파드 하나만 뜨고 Replica는 감감무소식. - 원인: Bitnami 차트의 기본 모드는
standalone이다. Replica 설정을 아무리 적어도architecture가 지정되지 않으면 무시된다. - 해결:
values.yaml최상단에architecture: replication추가.
Issue 2. FATAL: password authentication failed for user "repl_user"
- 현상: Primary는 정상인데 Replica 파드가 무한 재시작되며 로그에 인증 실패가 뜸. 비밀번호를 바꾼 적이 없음.
- 원인: “기존 데이터 잔존 문제”. 처음에 Standalone 모드로 띄웠을 때 PVC에 데이터가 생성되었는데, 이때는 복제가 필요 없어
repl_user가 생성되지 않음. 이후 설정을 바꿔 재배포했지만 데이터가 남아있어 초기화 스크립트가 스킵됨. - 해결: 개발 환경이므로 Primary용 PVC를 삭제하고 재배포하여 초기화 스크립트가 다시 돌게 함. (운영 환경이라면 Primary DB에 접속해
CREATE ROLE repl_user ...로 수동 생성 필요)
Issue 3. Replica Scale-out 시 ProvisioningFailed
- 현상: Replica 개수를 늘렸는데 파드가
Pending상태.storageclass.storage.k8s.io "postgresql-dev-pv" not found에러 발생. - 원인: Primary는 내가 만든 정적 PV를 쓰지만, Replica는 HPA로 늘어날 때마다 새로운 PV가 자동으로 생성되어야 한다. 그런데 존재하지 않는 StorageClass 이름을 적었거나,
selector를 남겨둬서 자동 생성을 막고 있었음. - 해결:
readReplicas.persistence.storageClass를 클러스터에 실제 존재하는"local-path"로 변경하고selector삭제.
Issue 4. “StatefulSet is invalid … Forbidden”
- 현상: 설정을 수정하고
helm upgrade를 쳤는데 업데이트 실패. - 원인: StatefulSet의
volumeClaimTemplates(스토리지 설정)은 Immutable(변경 불가) 필드다. 한 번 생성되면 StorageClass 등을 바꿀 수 없다. - 해결: 기존 Replica용 StatefulSet을 삭제하고 다시 배포.
🚀 배포 및 확인
모든 설정을 마치고 배포를 진행한다.
확인 1: 파드 상태
> kubectl get pods
---------------------------------------------------------------
NAME READY STATUS RESTARTS AGE
postgresql-dev-primary-0 1/1 Running 0 4h49m
postgresql-dev-read-0 1/1 Running 0 4h49m
확인 2: PV/PVC 연결 상태
- Primary는 내가 지정한
master-pv와 연결됨. - Replica는
pvc-xxxxx처럼 랜덤한 이름으로local-path를 통해 자동 생성됨.
구성 요약:
- Primary:
postgresql-dev-master-pvc를 통해/srv/k3s/data/postgresql/master경로에 영구 저장. - Replica:
local-pathStorageClass를 통해/srv/k3s/data/pvc-xxx경로에 자동 할당. - HPA: CPU 사용량에 따라 Replica 파드가 1~3개로 자동 조절됨.
📝 마무리
온프레미스 K3s 환경이라 스토리지가 제일 골치였는데, Primary는 정적 할당(안전성), Replica는 동적 할당(유연성/HPA)으로 혼합해서 구성하는 것이 정답이었다.
이제 부하 테스트를 걸어서 HPA가 작동하며 Replica 파드가 늘어나는지(그리고 PV가 자동으로 생기는지) 구경하러 가야겠다. 끝!