Zum Hauptinhalt springen

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 인스턴스를 서로 다른 노드에 강제 분산합니다. 단일 노드 장애 시 나머지 인스턴스는 영향을 받지 않습니다.


입주 서비스

서비스데이터베이스사용자용도
Teleportteleportpostgres사용자, 세션, 감사 로그
Grafanagrafanagrafana대시보드, 설정, 사용자
Umamiumamiumami웹 애널리틱스
Keycloakkeycloakkeycloak사용자, 클라이언트, 토큰
OpenClawopenclawopenclaw법률 문서 분석
Codercodercoder개발 환경 워크스페이스
Giteagiteagitea소스 저장소 메타데이터

mission_control 데이터베이스 및 역할은 2026-05-26에 완전히 삭제되었습니다 (mission-control 서비스 폐기). 이 클러스터에서 해당 항목은 더 이상 존재하지 않습니다.


연결 정보

CNPG 오퍼레이터는 클러스터 생성 시 아래 서비스를 자동으로 생성합니다.

서비스엔드포인트포트용도
Read/Writeinfra-db-rw.infra-db.svc.cluster.local5432Primary 연결 (쓰기)
Read Onlyinfra-db-ro.infra-db.svc.cluster.local5432Replica 연결 (읽기)
Anyinfra-db-r.infra-db.svc.cluster.local5432모든 인스턴스 라운드로빈

시크릿 관리

부트스트랩 자격 증명 (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-creds Secret으로 프로비저닝합니다.

백업

백업 아키텍처

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_upPostgreSQL 인스턴스 정상 여부
cnpg_pg_replication_lagReplica 복제 지연 시간 (초)
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

전체 클러스터 장애 복구 절차

  1. R2 버킷에서 최신 백업 및 WAL 아카이브 상태를 확인합니다.
  2. 위의 PITR 또는 베이스 스냅샷 복구 방법으로 새 클러스터를 생성합니다.
  3. 복구된 클러스터의 서비스 엔드포인트를 확인하고 애플리케이션 연결을 검증합니다.
  4. 데이터 무결성을 확인한 후 원본 클러스터를 삭제합니다.