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>
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.postInitApplicationSQL에CREATE 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 |
업스트림은 standard 가 system 과 기능 동등하며, barman-cloud 는
Barman Cloud Plugin 으로 대체하라고
명시한다. minimal/standard 는 애초에 백업 플러그인과 함께 쓰도록 설계된 이미지다.
현재 custom-values.yaml 은 18.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[].extensions 로 CREATE 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_libraries는Clusterspec 이라 유지되지만,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 는 Cluster 의 status.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.