security-catalog 프로젝트에서 첫 실사용 자체 빌드 이미지 3종을 포팅한다 — 전부
상위 태그·베이스 OS 교체로 해소 안 되는 CVE(Go 모듈 정적 링크 또는 미수정 CRITICAL/
HIGH)를 자체 빌드(소스 컴파일 또는 SUSE BCI 재설치)로 대응한다:
- images/cloudnative-pg: CNPG operator, release-1.30 소스 컴파일 + bci-micro
- images/cnpg-postgresql: PostgreSQL 18.4, bci-base + zypper 재설치
- images/etcd: etcd v3.7.1, 소스 컴파일(x/text 강제 업그레이드) + bci-micro
함께 추가:
- manifests/helm/{cloudnative-pg,cnpg-cluster,etcd} — 위 이미지를 참조하는 카탈로그 차트
- scripts/deploy-test/*.sh, .claude/deploy-test-procedure.md — CVE 0건과 별개로
"실제로 뜨는가"를 검증하는 배포 스모크 테스트
- .claude/pitfalls.md — 자체 빌드/배포 테스트 중 실측한 함정 모음
검토 중 발견해 반영한 수정:
- cloudnative-pg 차트의 image 블록을 etcd와 동일한 registry/repository/tag 3필드+
따옴표 포맷으로 통일 — 기존 포맷(repository에 registry+repo 결합, 따옴표 없음)은
patch-catalog-tag.py 의 split 패처가 tag만 갱신하고 repository는 그대로 남기는
조용한 부분 치환을 일으켜, 향후 레지스트리 마이그레이션 시 깨진 참조를 만들 수 있었다
- cnpg-cluster 차트의 SLES 커버리지 코멘트를 최신 실측(trivy가 SLES 15.7을 정상
커버함, 2026-07-29 재측정)에 맞게 정정 — 폐기된 "측정 불가/OVAL 우회 필요" 결론이
남아있었다
- CLAUDE.md/MEMORY.md 의 "images/ 디렉토리 없음" 서술을 갱신하고, 레지스트리
마이그레이션(docker.io/wbsong111 → docker.io/paasup)·decisions/analysis 문서 이관·
리소스 프로파일 추가를 다음 작업으로 기록
이 3개 이미지는 아직 dip-catalog 자체 CI(build-image.yml)로 빌드·게이트·push 를
실행해본 적이 없다 — 현재 참조 태그는 security-catalog 쪽에서 이미 검증된 것이다.
Co-Authored-By: Claude Sonnet 5 <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 에서 확인,doc/charts/cnpg/deploy-test.md검증 기록 참고). 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. 검증 이력
doc/charts/cnpg/deploy-test.md 참고.