# 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` 로 한다. - **`postInitApplicationSQL` vs `postInitApplicationSQLRefs`.** 둘 다 같은 시점(클러스터 생성 직후 1회)·같은 권한(앱 DB 안에서 superuser)으로 실행된다. 차이는 SQL 을 어디서 가져오느냐뿐이다 — 전자는 values 에 인라인, 후자는 ConfigMap/Secret 참조. 스키마 덤프처럼 큰 SQL 은 후자를 쓴다(values 에 수백 KB를 넣지 않아도 됨). 1회성이므로 **비멱등 SQL** (`CREATE TABLE` 등, `IF NOT EXISTS` 없이)을 그대로 넣어도 된다 — 재실행되지 않는다. 참조 순서는 Secret 전체 → ConfigMap 전체, 각 그룹 안에서는 배열 순서. ### 2) 배포 ```sh 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 --create-namespace ``` ### 3) 확인 ```sh # FQN 필수 — kubectl get cluster 는 Rancher/CAPI 리소스와 충돌한다 kubectl -n get clusters.postgresql.cnpg.io pg-cnpg kubectl -n get pods -l cnpg.io/cluster=pg-cnpg -L cnpg.io/instanceRole kubectl -n 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](https://github.com/cloudnative-pg/postgres-containers#image-types)) | 타입 | 내용 | 상태 | | --- | --- | --- | | `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](https://github.com/cloudnative-pg/plugin-barman-cloud) 으로 대체하라고 명시한다. `minimal`/`standard` 는 애초에 백업 플러그인과 함께 쓰도록 설계된 이미지다. **현재 `custom-values.yaml` 은 `18.4-system-trixie`(deprecated)를 쓰고 있다.** `standard-trixie` 로 전환하면 차단 CVE 가 32 → 23 건으로 줄어든다(barman 의 Python 스택 제거). 전환 전 백업 경로를 정해야 한다 — 아래 [8. 백업](#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 에 직접 질의한다. ```sh kubectl -n exec -c postgres -- \ psql -U postgres -d appdb -Atc "SELECT extname, extversion FROM pg_extension ORDER BY 1;" ``` **복구 방법 (검증됨).** `Database` 리소스를 삭제하고 재생성해 generation 을 초기화한다. `reclaimPolicy: retain` 이면 실제 DB 와 데이터는 보존된다(테스트에서 레코드 수 유지 확인). ```sh kubectl -n delete databases.postgresql.cnpg.io - helm upgrade ./ -f custom-values.yaml -n # 재생성 → reconcile 실행 ``` `reclaimPolicy: delete` 로 설정한 상태에서 이 절차를 쓰면 **실제 DB 가 삭제된다.** 반드시 `retain` 인지 먼저 확인한다. **운영 권고** - switchover·업그레이드 후에는 확장 존재 여부를 점검 항목에 넣는다. - 확장 유무에 기능이 의존하는 서비스(감사 로깅 등)는 확장 존재를 애플리케이션 레벨에서 헬스체크하거나, 위 점검을 모니터링으로 자동화한다. - `pgaudit` 처럼 보안 요건에 해당하는 확장이 조용히 사라지면 **감사 로그가 중단된다.** `shared_preload_libraries` 는 `Cluster` spec 이라 유지되지만, `CREATE EXTENSION` 이 풀리면 pgaudit 의 세션 감사 기능이 동작하지 않는다. ### 6) 인증·보안 | Name | 설명 | 기본값 | | --- | --- | --- | | `enableSuperuserAccess` | `false` 면 postgres superuser 시크릿이 아예 생성되지 않는다. 보안상 `false` 권장 | `false` | | `bootstrap.initdb.secretName` | 앱 계정 비밀번호를 담은 기존 시크릿. 미지정 시 operator 가 `-app` 에 무작위 생성 | `""` | operator 가 클러스터 CA(`-ca`), 서버 인증서(`-server`), 복제 인증서(`-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. 접속 | 서비스 | 대상 | | --- | --- | | `-rw` | primary — 읽기/쓰기 | | `-ro` | replica 만 — 읽기 전용 | | `-r` | 전체 인스턴스 — 읽기 라운드로빈 | | `-pooler-rw` | PgBouncer 경유 (pooler 활성화 시) | ```sh kubectl -n get secret -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 표준 동작이다. 시퀀스 연속성을 가정하는 애플리케이션은 영향을 받는다. ### 제거 ```sh helm uninstall -n # PVC 는 남는다. 데이터까지 지우려면 명시적으로 삭제한다. kubectl -n get pvc -l cnpg.io/cluster= ``` ## 5. 검증 이력 `doc/charts/cnpg/deploy-test.md` 참고.