infra-db
infra-db는 Lumie 인프라 구성 요소들이 공용으로 사용하는 PostgreSQL 클러스터입니다. CloudNativePG 오퍼레이터로 관리되며, 각 인프라 서비스는 독립된 데이터베이스와 전용 사용자를 통해 데이터를 격리합니다. WAL 아카이브 및 일별 스냅샷 백업은 Cloudflare R2로 전송됩니다.
개요
주요 사양
| 항목 | 값 |
|---|---|
| PostgreSQL 버전 | 18.1 (wal2json 플러그인 포함) |
| 인스턴스 수 | 3 (Primary 1 + Replica 2) |
| 스토리지 | local-path-retain / 인스턴스당 2Gi |
| 네임스페이스 | infra-db |
| 백업 대상 | Cloudflare R2 (s3://lumie-dr/cnpg/infra-db) |
| 백업 보존 기간 | 365일 |
| 모니터링 | PodMonitor (Prometheus) |
아키텍처
클러스터 구성
CNPG Cluster 매니페스트
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: infra-db
spec:
instances: 3
imageName: zot.lumie-infra.com/storage/postgresql-wal2json:18.1
bootstrap:
initdb:
database: teleport
owner: postgres
secret:
name: infra-db-bootstrap-secret
postInitSQL:
- "CREATE USER grafana WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE grafana OWNER grafana"
- "CREATE USER umami WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE umami OWNER umami"
- "CREATE USER keycloak WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE keycloak OWNER keycloak"
- "CREATE USER openclaw WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE openclaw OWNER openclaw"
- "CREATE USER coder WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE coder OWNER coder"
- "CREATE USER gitea WITH PASSWORD 'VAULT_PASSWORD'"
- "CREATE DATABASE gitea OWNER gitea"
storage:
storageClass: local-path-retain
size: 2Gi
resources:
requests:
memory: "384Mi"
cpu: "50m"
limits:
memory: "384Mi"
enableSuperuserAccess: true
postgresql:
parameters:
wal_level: "logical"
max_replication_slots: "8"
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: required
monitoring:
enablePodMonitor: true
backup:
barmanObjectStore:
destinationPath: "s3://lumie-dr/cnpg/infra-db"
endpointURL: "https://a0884dfaf71a61fe7cb952fb9d69eb68.r2.cloudflarestorage.com"
s3Credentials:
accessKeyId:
name: r2-barman-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: r2-barman-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 4
data:
compression: gzip
retentionPolicy: "365d"
주요 설정 설명
부트스트랩 전략
클러스터 최초 생성 시 initdb 방식으로 초기화합니다. 기본 데이터베이스는 teleport이며 소유자는 postgres(CNPG SUPERUSER)입니다. postInitSQL로 나머지 서비스의 사용자와 데이터베이스를 한 번에 프로비저닝합니다. 인프라-db에서는 앱 클러스터와 달리 서비스 전용 사용자가 각 DB의 소유자이며, 별도의 lumie_app 역할이 없습니다.
WAL 설정
wal_level = logical
max_replication_slots = 8
- logical WAL: 논리적 복제 및 CDC(Change Data Capture) 지원
- max_replication_slots: 최대 8개의 복제 슬롯 허용
안티 어피니티
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: required
required 타입으로 각 PostgreSQL 인스턴스를 서로 다른 노드에 강제 분산합니다. 단일 노드 장애 시 나머지 인스턴스는 영향을 받지 않습니다.
입주 서비스
| 서비스 | 데이터베이스 | 사용자 | 용도 |
|---|---|---|---|
| Teleport | teleport | postgres | 사용자, 세션, 감사 로그 |
| Grafana | grafana | grafana | 대시보드, 설정, 사용자 |
| Umami | umami | umami | 웹 애널리틱스 |
| Keycloak | keycloak | keycloak | 사용자, 클라이언트, 토큰 |
| OpenClaw | openclaw | openclaw | 법률 문서 분석 |
| Coder | coder | coder | 개발 환경 워크스페이스 |
| Gitea | gitea | gitea | 소스 저장 소 메타데이터 |
mission_control데이터베이스 및 역할은 2026-05-26에 완전히 삭제되었습니다 (mission-control 서비스 폐기). 이 클러스터에서 해당 항목은 더 이상 존재하지 않습니다.
연결 정보
CNPG 오퍼레이터는 클러스터 생성 시 아래 서비스를 자동으로 생성합니다.
| 서비스 | 엔드포인트 | 포트 | 용도 |
|---|---|---|---|
| Read/Write | infra-db-rw.infra-db.svc.cluster.local | 5432 | Primary 연결 (쓰기) |
| Read Only | infra-db-ro.infra-db.svc.cluster.local | 5432 | Replica 연결 (읽기) |
| Any | infra-db-r.infra-db.svc.cluster.local | 5432 | 모든 인스턴스 라운드로빈 |
시크릿 관리
부트스트랩 자격 증명 (VaultStaticSecret)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: infra-db-bootstrap-vss
spec:
vaultAuthRef: vault/vault-auth
mount: secret
type: kv-v2
path: infrastructure/postgresql
refreshAfter: 1h
destination:
create: true
name: infra-db-bootstrap-secret
type: kubernetes.io/basic-auth
transformation:
excludeRaw: true
templates:
username:
text: "{{ .Secrets.POSTGRES_USER }}"
password:
text: "{{ .Secrets.POSTGRES_PASSWORD }}"
- Vault KV v2 경로
secret/infrastructure/postgresql에서POSTGRES_USER,POSTGRES_PASSWORD를 읽어kubernetes.io/basic-auth타입 Secret으로 변환합니다. - 1시간마다 갱신됩니다.
Cloudflare R2 자격 증명 (VaultStaticSecret)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: r2-barman-vss
spec:
vaultAuthRef: vault/vault-auth
mount: secret
type: kv-v2
path: bootstrap/r2-barman
refreshAfter: 1h
destination:
create: true
name: r2-barman-creds
transformation:
excludeRaw: true
templates:
ACCESS_KEY_ID:
text: "{{ .Secrets.ACCESS_KEY_ID }}"
ACCESS_SECRET_KEY:
text: "{{ .Secrets.ACCESS_SECRET_KEY }}"
- Vault 경로
secret/bootstrap/r2-barman에서 R2 자격 증명을 읽어r2-barman-credsSecret으로 프로비저닝합니다.
백업
백업 아키텍처
infra-db는 두 가지 방식으로 Cloudflare R2에 백업됩니다.
| 방식 | 설명 | 압축 |
|---|---|---|
| WAL 아카이브 | 트랜잭션 단위로 지속적으로 R2에 전송 | gzip (병렬 4) |
| 일별 스냅샷 | ScheduledBackup으로 매일 04:00 KST 실행 | gzip |
ScheduledBackup
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: infra-db-daily
spec:
# 04:00 KST = 19:00 UTC
schedule: "0 0 19 * * *"
backupOwnerReference: self
cluster:
name: infra-db
immediate: false
- cron 표현식: CNPG는
CRON_TZ프리픽스를 지원하지 않으므로 UTC 시각으로 변환하여 입력합니다. - backupOwnerReference: self: 백업 오브젝트의 소유자를
ScheduledBackup으로 설정합니다 (ScheduledBackup 삭제 시 백업도 함께 삭제).
백업 상태 확인
# 스케줄 백업 목록 조회
kubectl get scheduledbackup -n infra-db
# 완료된 백업 목록 조회
kubectl get backup -n infra-db
# 특정 백업 상세 정보
kubectl describe backup <backup-name> -n infra-db
복구
Point-in-Time Recovery (PITR)
R2에 보관된 WAL 아카이브를 사용하여 특정 시점으로 복구할 수 있습니다.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: infra-db-restored
namespace: infra-db
spec:
instances: 4
imageName: zot.lumie-infra.com/storage/postgresql-wal2json:18.1
bootstrap:
recovery:
source: infra-db-backup
recoveryTarget:
targetTime: "2025-01-15 19:00:00" # UTC 기준
externalClusters:
- name: infra-db-backup
barmanObjectStore:
destinationPath: "s3://lumie-dr/cnpg/infra-db"
endpointURL: "https://a0884dfaf71a61fe7cb952fb9d69eb68.r2.cloudflarestorage.com"
s3Credentials:
accessKeyId:
name: r2-barman-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: r2-barman-creds
key: ACCESS_SECRET_KEY
storage:
storageClass: local-path-retain
size: 2Gi
베이스 스냅샷에서 복구
# 사용 가능한 백업 확인
kubectl get backup -n infra-db
# 특정 백업으로 새 클러스터 생성
kubectl apply -f - <<EOF
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: infra-db-restored
namespace: infra-db
spec:
instances: 4
bootstrap:
recovery:
backup:
name: infra-db-daily-20250115190000
EOF
ArgoCD 배포
Application 구성
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infra-db
namespace: argocd
spec:
project: default
sources:
- repoURL: https://github.com/Lumie-Edu/lumie-infra.git
targetRevision: main
path: storage/infra-db/manifests
destination:
server: https://kubernetes.default.svc
namespace: infra-db
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
managedNamespaceMetadata:
labels:
goldilocks.fairwinds.com/enabled: 'true'
- ServerSideApply: CNPG CRD의 대규모 매니페스트를 안전하게 적용하기 위해 Server-Side Apply를 사용합니다.
- goldilocks: VPA(Vertical Pod Autoscaler) 권장값 수집을 위해 네임스페이스에 레이블을 자동 설정합니다.
리소스 구성도
storage/infra-db/manifests/
├── kustomization.yaml # 리소스 목록
├── vault-static-secret.yaml # infra-db-bootstrap-secret 프로비저닝
├── r2-barman-vss.yaml # r2-barman-creds 프로비저닝
├── cluster.yaml # CNPG Cluster 정의
└── scheduled-backup.yaml # 일별 백업 스케줄
모니터링
PodMonitor
monitoring:
enablePodMonitor: true
CNPG가 자동으로 PodMonitor를 생성하여 Prometheus가 각 PostgreSQL 인스턴스의 메트릭을 수집합니다.
주요 메트릭
| 메트릭 | 설명 |
|---|---|
cnpg_pg_up | PostgreSQL 인스턴스 정상 여부 |
cnpg_pg_replication_lag | Replica 복제 지연 시간 (초) |
cnpg_pg_database_size_bytes | 데이터베이스별 크기 |
cnpg_pg_wal_files | 현재 WAL 파일 수 |
cnpg_backup_duration_seconds | 백업 소요 시간 |
Grafana 대시보드
대시보드 ID: 20417 (Grafana.com, 수동 임포트)
운영 가이드
클러스터 상태 확인
# 클러스터 전체 상태
kubectl get cluster -n infra-db
# 인스턴스 Pod 상태
kubectl get pods -n infra-db -l postgresql=infra-db
# Primary/Replica 역할 확인
kubectl get pods -n infra-db -l postgresql=infra-db \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.role}{"\n"}{end}'
복제 상태 확인
# Primary에서 복제 상태 조회
kubectl exec -n infra-db infra-db-1 -c postgres -- \
psql -c "SELECT application_name, state, sent_lsn, write_lsn, flush_lsn, replay_lsn FROM pg_stat_replication;"
# WAL 위치 확인
kubectl exec -n infra-db infra-db-1 -c postgres -- \
psql -c "SELECT pg_current_wal_lsn();"
페일오버 테스트
# Primary Pod 삭제 → CNPG가 자동으로 Replica를 승격
kubectl delete pod infra-db-1 -n infra-db
# 페일오버 진행 상황 모니터링
kubectl get cluster infra-db -n infra-db -w
스케일 조정
# 인스턴스 수 변경 (4 → 5)
kubectl patch cluster infra-db -n infra-db \
--type='merge' -p='{"spec":{"instances":5}}'
# 진행 상황 확인
kubectl get pods -n infra-db -w
로그 확인
# 특정 인스턴스 로그
kubectl logs infra-db-1 -n infra-db -c postgres
# CNPG Operator 로그
kubectl logs -n cnpg -l app.kubernetes.io/name=cloudnative-pg
문제 해결
Pod가 시작되지 않음
# 이벤트 확인
kubectl describe pod infra-db-1 -n infra-db
# PVC 상태 확인
kubectl get pvc -n infra-db
kubectl describe pvc postgres-infra-db-1 -n infra-db
복제 지연
# 복제 슬롯 상태 확인
kubectl exec -n infra-db infra-db-1 -c postgres -- \
psql -c "SELECT slot_name, active, restart_lsn FROM pg_replication_slots;"
# 복제 지연 확인
kubectl exec -n infra-db infra-db-1 -c postgres -- \
psql -c "SELECT now() - pg_last_xact_replay_timestamp() AS replication_delay;"
백업 실패
# 최근 백업 상태 확인
kubectl get backup -n infra-db --sort-by=.metadata.creationTimestamp
# 백업 오류 메시지 확인
kubectl describe backup <backup-name> -n infra-db
# R2 자격 증명 Secret 존재 여부 확인
kubectl get secret r2-barman-creds -n infra-db
전체 클러스터 장애 복구 절차
- R2 버킷에서 최신 백업 및 WAL 아카이브 상태를 확인합니다.
- 위의 PITR 또는 베이스 스냅샷 복구 방법으로 새 클러스터를 생성합니다.
- 복구된 클러스터의 서비스 엔드포인트를 확인하고 애플리케이션 연결을 검증합니다.
- 데이터 무결성을 확인한 후 원본 클러스터를 삭제합니다.