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>
8.3 KiB
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. 제거 참고). helm upgrade는 CRD 를 갱신하지 않는다. Helm 의 CRD 처리 방식 때문에 버전 업그레이드 시 CRD 를 수동 apply 해야 한다.- Kubernetes 1.25+ 필요. 검증 환경은 RKE2 v1.34.1 이다.
2) 배포
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) 확인
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 에 execroles/rolebindings생성 → 권한 상승 경로serviceaccounts,deployments조작mutatingwebhookconfigurations/validatingwebhookconfigurationspatch → 어드미션 제어 변경
이 조합은 실질적으로 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. 업그레이드
# 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 리소스를 삭제할 수 없게 된다.
# 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. 검증 이력
scripts/deploy-test/deploy-test-cnpg-cluster.sh 로 검증한다(절차: .claude/deploy-test-procedure.md).
RKE2 v1.34.1 / Longhorn 환경에서 3-instance 구성,
failover 3초, 데이터 정합성 유지를 확인했다.