Files
service-catalog/manifests/helm/cnpg-cluster/1.0.0/CUSTOM-README.md
T
wbsong111 1a747f61a8 자체 빌드 이미지 문서가 이 레포에 없는 경로를 인용하던 것을 없앤다 (#33) (#34)
images/·manifests/helm/·.claude/ 의 20개 파일이 doc/decisions·doc/analysis 등 **이 레포에
존재한 적 없는 경로 15종을 48곳에서** 인용하고 있었다. security-catalog 에서 포팅할 때
따라온 것인데, 그 레포는 개인 레포(github.com/wbsong111/security-catalog)라 팀 구성원은
접근조차 못 한다 — "security-catalog 에 있으나 이관되지 않았다" 는 안내가 아무 역할을
하지 못했다.

원문을 통째로 복사하지 않았다
----------------------------
원본 문서들이 서로를 근거로 인용한다. decisions/0001 하나만 봐도 analysis/cnpg-image-baseline.md
· analysis/vendor-unassessed-data-sources.md 처럼 **인용 목록에 없던 또 다른 미이관 문서**를
가리킨다. 복사는 문제를 옮기는 것이지 없애는 게 아니다.

그리고 대부분은 애초에 dip-catalog 가 더 나은 것을 갖고 있다. 7곳에서 인용되던
analysis/sles-oval-measurement.md 는 원문 스스로 "이 문서는 결정하지 않는다. 재측정하면
갱신된다" 고 밝히는 스냅샷인데, dip-catalog 는 같은 측정을 CoverageProbe 로 매 스캔마다
자동으로 한다. 문서를 복사하는 것보다 게이트를 가리키는 것이 정확하다.

그래서 성격별로 나눴다
---------------------
  재측정으로 복원 안 되는 것  →  doc/decisions/ 에 자립적 ADR 로 다시 씀 (4건)
  이미 단일 출처가 있는 것    →  그쪽으로 인용 교체 (11종 경로)

ADR 4건은 security-catalog 0001·0005·0006·0007 이 원본이고, 결론과 근거만 추려
dip-catalog 맥락으로 새로 썼다 — **레포 밖을 가리키는 링크가 0이다.** 번호는 이 레포에서
0001~0004 로 다시 붙였고 원본 대응은 각 문서와 README 에 적었다. 왜 안 가져온 것은 안
가져왔는지도 README 표에 남겼다.

인용 교체는 카테고리별로:
  analysis/*-cve.md, cnpg-image-vuln-comparison.md  →  해당 ADR · images/<image>/README.md
  analysis/sles-oval-measurement.md                 →  게이트 CoverageProbe (doc/sbom-pipeline.md)
  cve-zero-pipeline.md, architecture/build-pipeline.md → doc/sbom-pipeline.md
  image-selection.md                                →  .claude/image-authoring.md
  charts/*/deploy-test.md                           →  scripts/deploy-test/*.sh + 절차 문서

찾은 오류 2건
-------------
- images/cloudnative-pg/source.build.env 가 인용한 decisions/0004-cloudnative-pg-operator-self-build.md
  는 **번호 오기**다. 원본 0004 는 postgresql-chart-selection 이고 이 결정은 0005 다.
- cnpg-cluster values.yaml·templates/database.yaml 이 인용한 doc/deploy-test-cnpg.md 는
  **원본 레포에도 없다.** CREATE EXTENSION 함정 설명은 주석 자체에 이미 있어 인용만 뺐다.

검증
----
  우리 파일의 깨진 doc/ 인용        0건 (전수 스캔)
  새 문서·수정 문서의 로컬 링크     전부 실재 확인
  helm template                     cnpg-cluster · etcd · cloudnative-pg 정상 렌더

남은 doc/health-checking.md(144곳)·doc/integration/*(2곳)은 업스트림 CRD·차트 안의 문자열로
우리가 쓴 인용이 아니다 — 건드리지 않았다.

Closes #33

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:20:40 +09:00

14 KiB

cnpg-cluster 배포

차트 버전 1.0.0 / PostgreSQL 18

CloudNativePG 의 Cluster / Database / Pooler / ScheduledBackup 커스텀 리소스를 Helm 으로 감싼 PaaSup 자체 제작 차트다. 업스트림 차트가 아니다.

사전 조건: cloudnative-pg 오퍼레이터(차트 0.29.0 / operator 1.30.0)가 먼저 설치되어 있어야 한다. 오퍼레이터가 없으면 webhook 부재로 Cluster 생성 자체가 거부된다.

1. 배포 방법

1) 배포 시 주의 사항

  • instances 와 노드 수를 맞춘다. affinity.podAntiAffinityType: required 상태에서 노드 수가 instances 보다 적으면 Pod 가 Pending 에 머문다. 단일 노드 개발 환경은 preferred 로 내려야 한다.
  • backup.enabled: false 면 PITR 이 불가능하다. 또한 WAL 이 오브젝트 스토리지로 아카이브되지 않아 walStorage 볼륨에 계속 쌓인다. 운영 배포는 반드시 백업을 켠다.
  • 확장(extension)은 databases 로 선언한다. bootstrap.initdb.postInitApplicationSQLCREATE EXTENSION 을 넣으면 오류 없이 무시된다 (operator 1.30.0 배포 검증에서 확인).
  • bootstrap 은 최초 1회만 적용된다. 이미 생성된 클러스터의 bootstrap.initdb.database 를 바꿔도 아무 일도 일어나지 않는다. DB 추가는 databases 로 한다.

2) 배포

git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog/manifests/helm/cnpg-cluster/1.0.0
helm upgrade pg-cnpg ./ -f custom-values.yaml --install -n <namespace> --create-namespace

3) 확인

# FQN 필수 — kubectl get cluster 는 Rancher/CAPI 리소스와 충돌한다
kubectl -n <ns> get clusters.postgresql.cnpg.io pg-cnpg
kubectl -n <ns> get pods -l cnpg.io/cluster=pg-cnpg -L cnpg.io/instanceRole
kubectl -n <ns> get databases.postgresql.cnpg.io

2. custom-values.yaml 설명

1) 클러스터 규모

Name 설명 기본값
instances PostgreSQL 인스턴스 수. 1 = 단독(failover 불가), 3 = primary 1 + replica 2 3
primaryUpdateStrategy unsupervised = operator 가 자동 switchover 후 업데이트. supervised = 운영자 수동 승격 unsupervised
primaryUpdateMethod switchover = 정상 전환. restart = 제자리 재시작(다운타임 발생) switchover

2) 이미지

Name 설명 기본값
postgresql.image.repository PostgreSQL 이미지 저장소 ghcr.io/cloudnative-pg/postgresql
postgresql.image.tag 베이스 OS 를 포함한 태그를 써야 한다 (아래 경고 참고) 18.4-system-trixie
postgresql.imageName 전체 이미지 경로 직접 지정 (오프라인 미러). 지정 시 위 두 값은 무시된다 ""

이미지 타입 — system 은 deprecated 다

CNPG 는 세 가지 타입을 발행한다. (업스트림 README)

타입 내용 상태
minimal PostgreSQL 본체. PG18+ 는 JIT 없음, pgaudit·pgvector 없음 현행
standard + pgaudit, pgvector, pg-failover-slots, JIT, 전 로케일 현행 · 권장
system standard + barman-cloud 바이너리 deprecated

업스트림은 standardsystem기능 동등하며, barman-cloud 는 Barman Cloud Plugin 으로 대체하라고 명시한다. minimal/standard 는 애초에 백업 플러그인과 함께 쓰도록 설계된 이미지다.

현재 custom-values.yaml18.4-system-trixie(deprecated)를 쓰고 있다. standard-trixie 로 전환하면 차단 CVE 가 32 → 23 건으로 줄어든다(barman 의 Python 스택 제거). 전환 전 백업 경로를 정해야 한다 — 아래 8. 백업 참고.

경고 — 맨 major 태그(:18)를 쓰지 말 것. 실측 결과다.

태그 베이스 OS 지원 종료
ghcr.io/cloudnative-pg/postgresql:18 Debian 11 (bullseye) 2026-08-31
ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie Debian 13 (trixie) 2030-06-30

두 태그 모두 PostgreSQL 18.4 지만 베이스 OS 가 다르다. :18 로 배포하면 instance-manager 가 OS distribution is deprecated 를 로그로 남긴다. 보안 카탈로그 관점에서 EOL 베이스 이미지는 패치되지 않는 OS 패키지 CVE 를 그대로 안고 가는 것이므로 사용하지 않는다.

18.4-system-trixie 에 포함된 확장(실측): pgaudit 18.0, pg_stat_statements 1.12, pgcrypto 1.4, pg_trgm 1.6, vector 0.8.5.

참고로 imageName 을 아예 비우면 operator 1.30.0 이 18.4-system-trixie 를 기본값으로 채워준다. 다만 SBOM·재현성을 위해 카탈로그에서는 태그를 명시적으로 고정한다.

3) PostgreSQL 설정

Name 설명 기본값
postgresql.parameters postgresql.conf 파라미터 map. 값은 전부 문자열로 렌더링된다 custom-values.yaml 참고
postgresql.sharedPreloadLibraries preload 라이브러리 목록. operator 가 자체 항목과 병합한다 [pgaudit, pg_stat_statements]
postgresql.pg_hba pg_hba.conf 추가 규칙. CNPG 기본값은 TLS + scram-sha-256 []
postgresql.synchronous 동기 복제. 미설정 시 비동기(async). 운영은 {method: any, number: 1} 권장 미설정

sharedPreloadLibraries 에 올리는 것과 databases[].extensionsCREATE EXTENSION 하는 것은 별개다. pg_stat_statements 는 둘 다 필요하다 — preload 만 하면 뷰가 없고, extension 만 만들면 데이터가 수집되지 않는다.

4) 스토리지

Name 설명 기본값
storage.size / storage.storageClass 데이터 볼륨 10Gi / longhorn
walStorage.enabled WAL 을 별도 볼륨으로 분리. I/O 경합 감소 + WAL 폭증이 데이터 볼륨을 채우는 것을 방지 true
walStorage.size WAL 볼륨. 대략 데이터의 50%. 백업 미설정 시 더 크게 잡는다 5Gi

볼륨 크기는 축소할 수 없다. 티어별 값은 dip-volumes-quotas.yaml 참고.

5) 데이터베이스·확장 (Database CRD)

Name 설명 기본값
databases[].name DB 이름. bootstrap.initdb.database 와 같은 이름을 쓰면 그 DB 를 관리 대상으로 잡는다 -
databases[].owner 소유자. 생략 시 bootstrap.initdb.owner -
databases[].ensure present / absent present
databases[].reclaimPolicy retain = Database 리소스를 지워도 실제 DB 보존. delete = 함께 삭제 retain
databases[].extensions [{name, ensure, version, schema}] []
databases[].schemas [{name, ensure, owner}] []

중요 — 확장이 조용히 사라지는 문제 (operator 1.30.0 실측)

증상. primary switchover(failover, 롤링 이미지 업데이트) 이후 Database 로 선언한 확장이 전체 인스턴스에서 사라진다. 그런데 Database.status 는 계속 applied: true 로 남는다. 조용한 실패이므로 status 만 보면 정상으로 보인다.

배포 테스트에서 2회 모두 재현되었다 (failover 후 1회, 롤링 이미지 업데이트 후 1회). 데이터(테이블·레코드)는 정상 보존되며 확장만 유실된다.

원인. Database reconciler 는 spec generation 이 바뀔 때만 동작한다. 지속적으로 수렴(converge)시키지 않는다. 검증 내용:

  • DB 에서 수동으로 DROP EXTENSION → 2분간 관찰, operator 는 복구하지 않음. status.extensions[].applied 는 계속 true.
  • helm upgrade 로 동일한 Database 매니페스트 재적용 → spec 이 같으므로 generation 불변 → reconcile 이 돌지 않아 복구되지 않음.
  • kubectl annotate → generation 을 바꾸지 않으므로 효과 없음.

점검 방법. status 를 믿지 말고 DB 에 직접 질의한다.

kubectl -n <ns> exec <primary-pod> -c postgres -- \
  psql -U postgres -d appdb -Atc "SELECT extname, extversion FROM pg_extension ORDER BY 1;"

복구 방법 (검증됨). Database 리소스를 삭제하고 재생성해 generation 을 초기화한다. reclaimPolicy: retain 이면 실제 DB 와 데이터는 보존된다(테스트에서 레코드 수 유지 확인).

kubectl -n <ns> delete databases.postgresql.cnpg.io <release>-<dbname>
helm upgrade <release> ./ -f custom-values.yaml -n <ns>   # 재생성 → reconcile 실행

reclaimPolicy: delete 로 설정한 상태에서 이 절차를 쓰면 실제 DB 가 삭제된다. 반드시 retain 인지 먼저 확인한다.

운영 권고

  • switchover·업그레이드 후에는 확장 존재 여부를 점검 항목에 넣는다.
  • 확장 유무에 기능이 의존하는 서비스(감사 로깅 등)는 확장 존재를 애플리케이션 레벨에서 헬스체크하거나, 위 점검을 모니터링으로 자동화한다.
  • pgaudit 처럼 보안 요건에 해당하는 확장이 조용히 사라지면 감사 로그가 중단된다. shared_preload_librariesCluster spec 이라 유지되지만, CREATE EXTENSION 이 풀리면 pgaudit 의 세션 감사 기능이 동작하지 않는다.

6) 인증·보안

Name 설명 기본값
enableSuperuserAccess false 면 postgres superuser 시크릿이 아예 생성되지 않는다. 보안상 false 권장 false
bootstrap.initdb.secretName 앱 계정 비밀번호를 담은 기존 시크릿. 미지정 시 operator 가 <release>-app 에 무작위 생성 ""

operator 가 클러스터 CA(<release>-ca), 서버 인증서(<release>-server), 복제 인증서(<release>-replication)를 자동 발급한다. 인스턴스 간 통신은 mTLS 다. 컨테이너는 항상 non-root(uid 26)로 실행되며 차트에서 재정의할 값이 없다.

7) 배치

Name 설명 기본값
affinity.podAntiAffinityType required = 노드당 1개 강제(운영). preferred = 부족해도 스케줄(단일 노드 개발) preferred
affinity.topologyKey 분산 기준. 멀티 AZ 는 topology.kubernetes.io/zone kubernetes.io/hostname

8) 백업

CNPG 1.30 은 세 가지 백업 경로를 제공한다. 이 차트는 현재 첫 번째만 구현하고 있다.

경로 CRD 필드 이미지 내장 barman 이 차트 지원
오브젝트 스토리지 (in-core) spec.backup.barmanObjectStore 필요 (system 전용) (backup.*)
CSI 볼륨 스냅샷 spec.backup.volumeSnapshot 불필요 미구현
Barman Cloud Plugin spec.plugins[] + isWALArchiver 불필요 미구현

in-core barman 은 phase out 예정이다. standard/minimal 이미지로 전환하려면 아래 둘 중 하나를 구현해야 한다.

  • 플러그인: barman 이 사이드카 이미지에 있어 PostgreSQL 이미지와 분리된다. 업스트림 권장
  • CSI 스냅샷: VolumeSnapshotClass 가 선행 필요하다. 이 dev 클러스터는 VolumeSnapshot CRD 와 Longhorn CSI 드라이버는 있으나 클래스가 정의되어 있지 않다. 또한 스냅샷은 베이스 백업이므로 스냅샷 시점 사이로 복구(PITR)하려면 WAL 아카이빙이 별도로 필요하다
Name 설명 기본값
backup.enabled S3 호환 스토리지로 WAL 아카이브 + base backup (system 이미지 필요) false
backup.retentionPolicy 보존 기간 30d
backup.barmanObjectStore.destinationPath 예: s3://pg-backup/cnpg ""
backup.barmanObjectStore.endpointURL MinIO/RustFS 등 사설 S3 엔드포인트 ""
scheduledBackup.enabled 정기 백업 활성화 (backup.enabled: true 필요) false
scheduledBackup.schedule 6필드 cron (초 분 시 일 월 요일). 표준 5필드가 아니다 "0 0 2 * * *"

9) 커넥션 풀러

Name 설명 기본값
pooler.enabled PgBouncer 배포 false
pooler.type rw (primary) / ro (replica) rw
pooler.poolMode transaction 권장. session 은 풀링 효과가 낮다 transaction

3. 접속

서비스 대상
<release>-rw primary — 읽기/쓰기
<release>-ro replica 만 — 읽기 전용
<release>-r 전체 인스턴스 — 읽기 라운드로빈
<release>-pooler-rw PgBouncer 경유 (pooler 활성화 시)
kubectl -n <ns> get secret <release>-app -o jsonpath='{.data.password}' | base64 -d

4. 운영

failover

primary Pod 손실 시 operator 가 자동으로 replica 를 승격한다. 실측 2~3초. 구 primary 는 재기동 후 replica 로 자동 재합류한다.

수동 switchover 는 Clusterstatus.targetPrimary 를 직접 바꾸지 않고 kubectl cnpg promote 플러그인을 쓴다. 플러그인이 없으면 primary Pod 를 삭제하는 방식으로 대체할 수 있다(계획된 전환에는 권장하지 않음).

시퀀스 주의

failover 후 serial/identity 시퀀스 값이 점프한다(실측 1 → 34). WAL 에 기록되지 않은 시퀀스 캐시 블록이 유실되는 PostgreSQL 표준 동작이다. 시퀀스 연속성을 가정하는 애플리케이션은 영향을 받는다.

제거

helm uninstall <release> -n <ns>
# PVC 는 남는다. 데이터까지 지우려면 명시적으로 삭제한다.
kubectl -n <ns> get pvc -l cnpg.io/cluster=<release>

5. 검증 이력

scripts/deploy-test/deploy-test-cnpg-cluster.sh 로 검증한다 — 절차와 통과 기준은 .claude/deploy-test-procedure.md.