# CloudNativePG Operator 배포 차트 버전 `0.29.0` / operator 버전 `1.30.0` CloudNativePG 는 PostgreSQL 을 Kubernetes 오퍼레이터로 운영하는 CNCF 프로젝트다. 이 차트는 **오퍼레이터만** 설치한다. 실제 DB 클러스터는 `cnpg-cluster` 차트로 배포한다. ``` cloudnative-pg (operator) → CRD + controller + webhook ↓ 감시 cnpg-cluster (Cluster CR) → PostgreSQL primary/replica Pod, PVC, Service ``` ## 1. 배포 방법 ### 1) 배포 시 주의 사항 - **CRD 와 webhook 은 cluster-scoped 리소스다.** 네임스페이스를 분리해도 클러스터 전체에 하나만 존재한다. 여러 팀이 각자 오퍼레이터를 설치하면 CRD 버전이 충돌한다. 클러스터당 오퍼레이터는 1개만 둔다. - **webhook 의 `failurePolicy` 가 `Fail` 이다.** 오퍼레이터 Pod 가 없는 상태에서는 `Cluster` 리소스의 생성·수정·삭제가 모두 거부된다. 오퍼레이터를 제거할 때는 webhook 설정을 반드시 함께 삭제해야 한다 (아래 [4. 제거](#4-제거) 참고). - **`helm upgrade` 는 CRD 를 갱신하지 않는다.** Helm 의 CRD 처리 방식 때문에 버전 업그레이드 시 CRD 를 수동 apply 해야 한다. - Kubernetes 1.25+ 필요. 검증 환경은 RKE2 v1.34.1 이다. ### 2) 배포 ```sh git clone https://github.com/paasup/dip-catalog.git cd dip-catalog/manifests/helm/cloudnative-pg/0.29.0 helm upgrade cnpg ./ -f custom-values.yaml --install -n cnpg-system --create-namespace --wait ``` ### 3) 확인 ```sh kubectl -n cnpg-system get pods kubectl get crd | grep cnpg # 11개 ``` > `kubectl get cluster` 는 쓰지 않는다. Rancher(`clusters.management.cattle.io`) 와 > CAPI(`clusters.cluster.x-k8s.io`) 가 같은 단축명을 쓰기 때문에 엉뚱한 리소스가 조회된다. > 반드시 `kubectl get clusters.postgresql.cnpg.io` 로 FQN 을 쓴다. ## 2. custom-values.yaml 설명 ### 1) 이미지 설정 | Name | 설명 | 기본값 | | --- | --- | --- | | `image.repository` | 오퍼레이터 이미지. 오프라인 환경에서는 사내 미러 경로로 변경 | `ghcr.io/cloudnative-pg/cloudnative-pg` | | `image.tag` | 미설정 시 차트 `appVersion`(1.30.0) 사용. 버전 변경은 차트 교체를 우선한다 | `""` | | `imagePullSecrets` | 사설 레지스트리 인증 시크릿 | `[]` | ### 2) 감시 범위 (RBAC 영향) | Name | 설명 | 기본값 | | --- | --- | --- | | `config.clusterWide` | `true` = ClusterRole 로 전체 네임스페이스 감시. `false` = 설치 네임스페이스만 감시하고 RBAC 이 Role 로 축소됨 | `true` | | `config.data.WATCH_NAMESPACE` | `clusterWide: true` 상태에서 감시 대상을 특정 네임스페이스로 한정 (쉼표 구분) | 미설정 | | `config.maxConcurrentReconciles` | 동시 reconcile 수 | `10` | #### 보안 관점 — 실측 RBAC 비교 (operator 1.30.0) `clusterWide` 는 감시 범위와 **RBAC 범위를 함께** 바꾼다. 실제로 렌더링해 측정한 결과다. | | `clusterWide: true` | `clusterWide: false` | | --- | --- | --- | | ClusterRole 규칙 수 | 다수 (전 리소스) | **3개** | | ClusterRole 이 다루는 리소스 | `secrets`, `pods`, `pods/exec`, `serviceaccounts`, `roles`, `rolebindings`, `deployments`, `configmaps`, PVC, webhook 설정, `nodes` … | `nodes`(RO), `clusterimagecatalogs`(RO), webhook 설정(get/patch) | | 네임스페이스 Role | 없음 | 생성됨 (20 규칙, 설치 NS 한정) | | ClusterRoleBinding | 생성됨 | 생성됨 (축소된 ClusterRole 에 바인딩) | **`clusterWide: true` 의 실제 위험도:** 오퍼레이터 ServiceAccount 가 클러스터 전체에 대해 다음을 갖는다. - `secrets` 전체 CRUD → 모든 네임스페이스의 모든 시크릿 열람 (Harbor·Keycloak·Infisical 토큰 포함) - `pods/exec` → 임의 네임스페이스의 임의 Pod 에 exec - `roles` / `rolebindings` 생성 → **권한 상승 경로** - `serviceaccounts`, `deployments` 조작 - `mutatingwebhookconfigurations` / `validatingwebhookconfigurations` patch → 어드미션 제어 변경 이 조합은 실질적으로 **cluster-admin 에 준한다.** 오퍼레이터 Pod 가 침해되면 클러스터 전체가 침해된다고 봐야 한다. **`WATCH_NAMESPACE` 는 보안 경계가 아니다.** 이 값은 오퍼레이터가 *reconcile 할 대상*만 좁힌다. RBAC 은 그대로 cluster-wide 로 남으므로 토큰의 권한은 줄어들지 않는다. 심층 방어(defense-in-depth) 수단일 뿐, 권한 축소로 오해하면 안 된다. **권고** - **보안이 우선이면 `clusterWide: false`** — 테넌트 네임스페이스마다 오퍼레이터를 따로 설치한다. 다만 CRD 와 webhook 설정은 cluster-scoped 싱글턴이므로 **모든 오퍼레이터의 버전이 같아야 하고**, 각 설치가 동일한 webhook 설정을 patch 하려고 경쟁한다. 운영 복잡도가 크게 올라간다. - **운영 편의가 우선이면 `clusterWide: true`** — 단, 위 권한을 감수하는 결정임을 명시하고 다음 보완책을 함께 적용한다. - 오퍼레이터 네임스페이스에 접근 가능한 주체를 최소화 (`cnpg-system` 을 별도 관리) - 오퍼레이터 Pod 의 exec/attach 를 Kyverno 등으로 차단 - 감사 로그에서 오퍼레이터 SA 의 `secrets` 접근을 모니터링 - `clusterimagecatalogs` 로 허용 이미지를 고정해 임의 이미지 기동을 막는다 배포 테스트에서는 격리를 위해 `clusterWide: false` 를 사용했다. `custom-values.yaml` 기본값은 업스트림과 동일한 `true` 이므로, 도입 시 이 결정을 반드시 검토해야 한다. ### 3) Webhook | Name | 설명 | 기본값 | | --- | --- | --- | | `webhook.port` | webhook 서비스 포트 | `9443` | | `webhook.mutating.create` | mutating webhook 생성 여부 | `true` | | `webhook.validating.create` | validating webhook 생성 여부 | `true` | | `webhook.*.failurePolicy` | `Fail` 유지 권장. `Ignore` 로 바꾸면 검증 없이 잘못된 Cluster 스펙이 통과된다 | `Fail` | ### 4) 모니터링 | Name | 설명 | 기본값 | | --- | --- | --- | | `monitoring.podMonitorEnabled` | Prometheus Operator CRD 필요. 없으면 배포 실패 | `false` | | `monitoring.grafanaDashboard.create` | Grafana 대시보드 ConfigMap 생성 | `false` | ### 5) 리소스 | Name | 설명 | 기본값 | | --- | --- | --- | | `resources` | 오퍼레이터 Pod 의 cpu/memory. 티어별 값은 `dip-resources-quotas.yaml` 참고 | 업스트림은 `{}` (무제한) | | `replicaCount` | leader election 기반이라 2 이상은 가용성 목적 (reconcile 은 리더 1개가 수행) | `1` | ## 3. 업그레이드 ```sh # 1) CRD 를 먼저 수동 갱신 (helm upgrade 는 CRD 를 건드리지 않음) kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.30.0/releases/cnpg-1.30.0.yaml \ --dry-run=server # 먼저 dry-run 으로 영향 확인 # 2) 차트 업그레이드 helm upgrade cnpg ./ -f custom-values.yaml -n cnpg-system --wait ``` 오퍼레이터 업그레이드는 실행 중인 `Cluster` 의 인스턴스를 롤링 재시작시킨다. `primaryUpdateStrategy` 설정에 따라 primary 전환이 발생하므로 서비스 영향 시간을 고려해야 한다. ## 4. 제거 **순서가 중요하다.** webhook 이 `failurePolicy: Fail` 이므로 오퍼레이터를 먼저 지우면 `Cluster` 리소스를 삭제할 수 없게 된다. ```sh # 1) 먼저 모든 Cluster 리소스 삭제 kubectl get clusters.postgresql.cnpg.io -A kubectl -n delete clusters.postgresql.cnpg.io # 2) 오퍼레이터 제거 helm uninstall cnpg -n cnpg-system # 3) cluster-scoped 잔여물 제거 (helm uninstall 로 남는다) kubectl delete validatingwebhookconfiguration cnpg-validating-webhook-configuration --ignore-not-found kubectl delete mutatingwebhookconfiguration cnpg-mutating-webhook-configuration --ignore-not-found kubectl get crd -o name | grep cnpg.io | xargs -r kubectl delete ``` > CRD 삭제는 해당 CRD 의 모든 리소스를 삭제한다. PVC 는 남지만 `Cluster` 정의는 사라지므로 > 운영 클러스터에서는 3단계를 실행하기 전에 반드시 백업을 확인한다. ## 5. 검증 이력 `doc/charts/cnpg/deploy-test.md` 참고. RKE2 v1.34.1 / Longhorn 환경에서 3-instance 구성, failover 3초, 데이터 정합성 유지를 확인했다.