Files
service-catalog/manifests/helm/cloudnative-pg/0.29.0/CUSTOM-README.md
T
wbsong111 6878b61efa 자체 빌드 이미지 3종(cloudnative-pg/cnpg-postgresql/etcd) + 대응 헬름 차트 도입
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>
2026-08-03 12:03:37 +09:00

170 lines
8.3 KiB
Markdown

# 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 <ns> delete clusters.postgresql.cnpg.io <name>
# 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초, 데이터 정합성 유지를 확인했다.